How to Create Service Account in Kubernetes and VMware TKGI (With Token and Kubeconfig)

Last updated: March 2026

Managing secure access to Kubernetes clusters is critical, especially when giving third-party vendors read-only access. In this guide, we’ll show you step-by-step how to create a service account (SA) with read-only permissions, generate a bearer token (Access token), and create a working kubeconfig for both vanilla Kubernetes and VMware TKGI clusters.

We’ll also cover common pitfalls like RBAC restrictions and PEM parsing issues with CA certificates.

Background

  • Kubernetes service accounts allow automated processes or external tools to authenticate and interact with the cluster.
  • Modern Kubernetes versions (v1.24+) no longer automatically store service account secrets as accessible Secret objects. Instead, kubectl create token <sa> is the recommended method for generating tokens.
  • By default, the built-in view role allows namespaced read-only access (pods, deployments, services, etc.) but does not include cluster-level resources like nodes.

Part 1 (Using Imperative approach): Vanilla Kubernetes Cluster

Step 1 – Connect to Your Cluster

Make sure you are logged in as a cluster admin:

ansu@mastern-01:~$ kubectl config current-context

kubernetes-admin@kubernetes

Step 2 – Create the Service Account

ansu@mastern-01:~$ kubectl create serviceaccount cimtrack-sa -n default

serviceaccount/cimtrack-sa created
ansu@mastern-01:~$ kubectl get sa cimtrack-sa -n default

NAME          SECRETS   AGE
cimtrack-sa   0         3m25s

Step 3 – Bind Read-Only Role

Bind the service account to the built-in view cluster role:

ansu@mastern-01:~$ kubectl create clusterrolebinding cimtrack-sa-readonly \
--clusterrole=view \
--serviceaccount=default:cimtrack-sa

clusterrolebinding.rbac.authorization.k8s.io/cimtrack-sa-readonly created

where,

cimtrack-sa-readonly = clusterolebinding name

cimtrack-sa = service account name

default = namespace where the service account name will be created.

ansu@mastern-01:~$ kubectl get clusterrolebinding cimtrack-sa-readonly

NAME                   ROLE               AGE
cimtrack-sa-readonly   ClusterRole/view   31s

Step 4 – Generate the bearer/access Token

for the – -duration flag, it means I want the token to be valid for 8760 hours which is one year. You can use any duration you want to

ansu@mastern-01:~$ kubectl create token cimtrack-sa -n default --duration=8760h

eyJhbGciOiJSUzI1NiIsImtpZCI6IkhSOE16cElXQVlVZGVvSi1xY2NKN21hZnBpalpNanBmMF9JSDJhVFo3dWsifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxODA0Njg1NDU0LCJpYXQiOjE3NzMxNDk0NTQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZW

Copy the token — this is your bearer token.

⚠️ Note: Kubernetes no longer stores service account secrets as Secret objects for tokens by default. You must use kubectl create token to generate them.

We can optionally test if the token is working

Step 5 – Create a Kubeconfig for Testing

Declare your variables

CLUSTER_NAME=$(kubectl config current-context)
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
TOKEN=<paste-your-token-here>

The command will look like this.

ansu@mastern-01:~$ CLUSTER_NAME=$(kubectl config current-context)
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
CA_CERT=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')
TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IkhSOE16cElXQVlVZGVvSi1xY2NKN21hZnBpalpNanBmMF9JSDJhVFo3dWsifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxODA0Njg1NDU0LCJpYXQiOjE3NzMxNDk0NTQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZW

Create the kubeconfig file

ansu@mastern-01:~$ cat <<EOF > cimtrack-sa.kubeconfig
apiVersion: v1
kind: Config
clusters:
- cluster:
    server: ${APISERVER}
    insecure-skip-tls-verify: true
  name: ${CLUSTER_NAME}
contexts:
- context:
    cluster: ${CLUSTER_NAME}
    user: cimtrack-sa
  name: cimtrack-sa-context
current-context: cimtrack-sa-context
users:
- name: cimtrack-sa
  user:
    token: ${TOKEN}
EOF

Step 6 – Test the Service Account

ansu@mastern-01:~$ kubectl --kubeconfig=cimtrack-sa.kubeconfig get pods -A

NAMESPACE          NAME                                               READY   STATUS      RESTARTS           AGE
argocd             argocd-application-controller-0                    0/1     Running     1 (42d ago)        256d
argocd             argocd-applicationset-controller-bdd44f68b-cl7hp   1/1     Running     1 (42d ago)        256d
argocd             argocd-dex-server-5d77b5674c-nlxz2                 1/1     Running     0                  38d
argocd             argocd-notifications-controller-644d599cff-6mct4   1/1     Running     0                  38d
argocd             argocd-redis-58c46c8f79-cv8h6                      1/1     Running     0                  38d
argocd             argocd-repo-server-5bf7c558f7-kxb9l                1/1     Running     0                  38d

If testing doesn’t work, you may need to run this command below, then test again

ansu@mastern-01:~$ kubectl config set-cluster test-cluster \
  --server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') \
  --certificate-authority=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.certificate-authority}') \
  --kubeconfig=cimtrack-sa.kubeconfig
Cluster "test-cluster" set.

Part 1B (Using Declarative approach): Vanilla Kubernetes Cluster

I will be creating all the objects/resources in the default namespace because I want the cimtrack tool to be able to access all the folders/objects in every namespaces in the cluster.

1. create a service account

ansu@mastern-01:~$ vi sa.yaml

eqapiVersion: v1
kind: ServiceAccount
metadata:
  name: testingteam
  namespace: default

ansu@mastern-01:~$ k apply -f sa.yaml

serviceaccount/testingteam created

2. Create a clusterrolebinding

ansu@mastern-01:~$ vi crb.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: testingteam-default-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: testingteam
  namespace: default
ansu@mastern-01:~$ k apply -f crb.yaml

clusterrolebinding.rbac.authorization.k8s.io/testingteam-default-binding created

What this does

This binds the testingteam ServiceAccount to the cluster-admin ClusterRole, which means:

  • Full cluster access
  • Can manage nodes, pods, deployments, RBAC, etc.
  • Equivalent to cluster administrator

Important (especially for production):
Giving cluster-admin is very powerful and usually avoided unless absolutely necessary.

3. Create a secret

ansu@mastern-01:~$ vi secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: testingteam-token
  namespace: default
  annotations:
    kubernetes.io/service-account.name: testingteam
type: kubernetes.io/service-account-token
ansu@mastern-01:~$ k apply -f secret.yaml

secret/testingteam-token created

Important note (modern Kubernetes)

In Kubernetes v1.24+, service accounts do NOT automatically create secrets anymore. The recommended method is:

kubectl create token testingteam -n default

The token will only last for 1 hour with the above command, you can specify the duration you want the token to last just as we did in part1A

Your static secret method still works, but Kubernetes now prefers short-lived tokens generated on demand for better security.

You will notice that in part1A, we didn’t create a secret because we don’t want the token to last forever.

4. Generate, extract and print the token

ansu@mastern-01:~$ TOKEN=$(kubectl -n default get secret testingteam-token -o jsonpath='{.data.token}' | base64 -d)
ansu@mastern-01:~$ echo $TOKEN

eyJhbGciOiJSUzI1NiIsImtpZCI6IkhSOE16cElXQVlVZGVvSi1xY2NKN21hZnBpalpNanBmMF9JSDJhVFo3dWsifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlY

5. When creating a service account, especially for a third application outside of the cluster to integrate with, its a good idea to always confirm your cluster API enpoint/server which can be given to the third party vendor.

ansu@mastern-01:~$  kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'

https://10.10.10.145:6443ansu@mastern-01:~$

This is the IP of my LB (layer 4) – The one that load-balances the cluster control plane. if you created a DNS record for it, and its resolvable in your environment, that can also be used.

what you are trying to do is to extract the Kubernetes API server URL from your kubeconfig.

What this command is doing

You are trying to extract the Kubernetes API server URL from your kubeconfig.

Breakdown:

  • kubectl config view → shows your kubeconfig
  • --minify → shows only the current context
  • -o jsonpath=... → extracts a specific field
  • .clusters[0].cluster.server → the API server endpoint

Part 2: TKGI Cluster

Creating a service account in Vanilla Kubernetes and VMware TKGI are the same thing, the same steps. I wanted to try it out in TKGI so I created it using Vanilla kubernetes first and the follwing steps below are what i did to create it on TKGI, just as I have laid it step by step above.

Steps:

# kubectl create sa team1 -n development

#kubectl create clusterrolebinding team1-ro --clusterrole=view --serviceaccount=development:team1

# kubectl create token team1 -n development --duration=8760h

#create the kubeconfig for testing 

 
#define the variables
TOKEN=12345678910
CLUSTER_NAME=$(kubectl config current-context)
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')


#create the kubeconfig file

cat <<EOF > testing-team.kubeconfig

apiVersion: v1

kind: Config

clusters:

- cluster:

    server: ${APISERVER}

    insecure-skip-tls-verify: true

  name: ${CLUSTER_NAME}

contexts:

- context:

    cluster: ${CLUSTER_NAME}

    user: team1

  name: development

current-context: develop #I used the cluster name which is the context for me

users:

- name: team1

  user:

    token: ${TOKEN}

EOF

#test the access

# kubectl –kubeconfig=testing-team.tkgi.kubeconfig get pods -A
# kubectl –kubeconfig=testing-team.tkgi.kubeconfig get namespaces

Friendly commands

ansu@mastern-01:~$ kubectl  get sa
NAME          SECRETS   AGE
cimtrack-sa   0         2d1h
default       0         264d
testingteam   0         110m
ansu@mastern-01:~$ kubectl  get sa cimtrack-sa -o yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2026-03-10T13:24:49Z"
  name: cimtrack-sa
  namespace: default
  resourceVersion: "65636552"
  uid: d093fe19-93db-4f77-ab95-cd445c088aa1
ansu@mastern-01:~$ kubectl  get sa testingteam -o yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"testingteam","namespace":"default"}}
  creationTimestamp: "2026-03-12T13:12:01Z"
  name: testingteam
  namespace: default
  resourceVersion: "66101435"
  uid: 6a9f7e16-2e81-4f06-9344-1453e44a4816
ansu@mastern-01:~$ k get clusterrolebinding |grep testing

testingteam-default-binding                              ClusterRole/cluster-admin  
ansu@mastern-01:~$ k get clusterrolebinding |grep cimtrack

cimtrack-sa-readonly                                     ClusterRole/view                                                                   2d1h

ansu@mastern-01:~$ k get clusterrolebinding testingteam-default-binding -o yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRoleBinding","metadata":{"annotations":{},"name":"testingteam-default-binding"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"cluster-admin"},"subjects":[{"kind":"ServiceAccount","name":"testingteam","namespace":"default"}]}
  creationTimestamp: "2026-03-12T13:17:27Z"
  name: testingteam-default-binding
  resourceVersion: "66102307"
  uid: 0f3d6b88-2fea-44b3-a7cd-2209dd0c90ac
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: testingteam
  namespace: default

ansu@mastern-01:~$ k get clusterrolebinding cimtrack-sa-readonly -o yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: "2026-03-10T13:28:58Z"
  name: cimtrack-sa-readonly
  resourceVersion: "65637225"
  uid: a4c3a5a0-db79-45e0-8516-a0f5a7947125
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: ServiceAccount
  name: cimtrack-sa
  namespace: default

Be the first to comment

Leave a Reply

Your email address will not be published.


*