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
Secretobjects. Instead,kubectl create token <sa>is the recommended method for generating tokens. - By default, the built-in
viewrole 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
Secretobjects for tokens by default. You must usekubectl create tokento 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
Leave a Reply