Prerequisites: kubernetes

Every request that reaches the Kubernetes API server is authenticated before it is evaluated: the server first establishes who is asking, then decides whether they may. When you run kubectl from the workstation, that identity comes from the admin kubeconfig. Code running inside a pod has no kubeconfig, and it should not borrow yours: a job that only needs to read four resource kinds must not act as cluster admin because a human credential was baked into its image. A ServiceAccount is the in-cluster identity built for this: a small namespaced object whose whole job is to be someone the API server can recognize. Official reference: service accounts.

Every pod runs as one

Each namespace gets a ServiceAccount named default at creation, and every pod runs as exactly one ServiceAccount: whatever spec.serviceAccountName names, or default if the field is unset. There is no such thing as a pod without an identity. See it on devata:

kubectl get serviceaccounts -n kube-system | head
kubectl get pod -n kube-system -l app.kubernetes.io/name=sealed-secrets \
  -o jsonpath='{.items[0].spec.serviceAccountName}'

The second command prints sealed-secrets: the controller you installed in installing-sealed-secrets runs as its own ServiceAccount, not as default, precisely so its permissions can be scoped to it alone.

The token, and why kubectl works inside a pod

The kubelet mounts a credential for the pod’s ServiceAccount into every container at a fixed path:

/var/run/secrets/kubernetes.io/serviceaccount/
├── ca.crt        the cluster CA, to verify the apiserver
├── namespace     the pod's own namespace, as plain text
└── token         a signed JWT naming the ServiceAccount

Every Kubernetes client library, kubectl included, looks for that path when it finds no kubeconfig. This is the entire reason a container can run kubectl get nodes with zero configuration: it is not magic, it is a well-known file. The token is not a stored password; it is minted on demand by the TokenRequest API, expires (one hour by default), is bound to that specific pod, and the kubelet refreshes the file before expiry. Decode one and read it:

kubectl run tokenpeek --rm -i --restart=Never --image=alpine:3.21 --command -- \
  sh -c 'cut -d. -f2 /var/run/secrets/kubernetes.io/serviceaccount/token | base64 -d 2>/dev/null; echo'

The JWT payload names the issuer, the expiry, the bound pod, and a sub field like system:serviceaccount:default:default. That string is the ServiceAccount’s username, and its shape is always system:serviceaccount:<namespace>:<name>. It appears again wherever an identity must be named: in rbac bindings and in kubectl auth can-i --as=... checks.

Identity is not permission

A ServiceAccount grants nothing by itself. A fresh one can authenticate perfectly and then be denied everything, because authorization is a separate step answered by rbac. This split is what makes least privilege possible: create a dedicated identity, then grant it exactly the verbs it needs and nothing else.

Creating one is two lines, and it is deliberately boring:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: snapshot-publisher
  namespace: showcase

The interesting part, what this identity is allowed to do, lives entirely in rbac.