Kubernetes ServiceAccounts with Examples

Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Any host with kubectl configured; any Kubernetes cluster
Cert prep CKA · CKAD
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope ServiceAccount creation and assignment, projected token mounts, kubectl create token, automountServiceAccountToken, custom projected audiences, RBAC identity checks with kubectl auth whoami and kubectl auth can-i, and imagePullSecrets on ServiceAccounts. Does not cover full Role YAML, OIDC, human users, token signing keys, or cloud workload identity.
Related guides Kubernetes Secrets

Every Pod created through the Kubernetes API is assigned a ServiceAccount. Static Pods are an exception because their specifications cannot reference ServiceAccounts or other API objects. That identity is how an in-cluster workload authenticates to the API server; RBAC decides what it may do afterward. This walkthrough follows the lifecycle: inspect the default account, create a custom one, read the projected token mount, request a short-lived token, disable automount, and test authorization separately from identity.


Understand Kubernetes ServiceAccount Identity

Default and custom ServiceAccounts

When you omit serviceAccountName, Kubernetes assigns the namespace default ServiceAccount. ServiceAccounts are namespace-scoped API objects—you cannot reference one from another namespace.

Static Pods are managed directly by the kubelet and cannot reference a ServiceAccount.

ServiceAccount identity versus RBAC permission

A ServiceAccount answers who the Pod is when it calls the API. It does not grant permissions by itself. For how authentication and authorization fit in the request path, see authentication and admission control.

Without a matching RBAC binding, the ServiceAccount normally lacks workload permissions such as listing Pods. It may still have default API-discovery permissions or permissions granted through existing group bindings.


Inspect the Default ServiceAccount Token Mount

Assigned ServiceAccount

I use namespace application and a Pod with no serviceAccountName:

bash
kubectl create namespace application

Sample output:

output
namespace/application created

Wait for the namespace-generated objects required by later examples:

bash
kubectl wait --for=create serviceaccount/default -n application --timeout=60s
bash
kubectl wait --for=create configmap/kube-root-ca.crt -n application --timeout=60s

The default ServiceAccount is created for each namespace, and the custom projected-volume example later references kube-root-ca.crt.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: default-identity
  namespace: application
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
  restartPolicy: Never

Apply the manifest and wait until the Pod is Ready:

bash
kubectl apply -f default-identity.yaml
bash
kubectl wait --for=condition=Ready pod/default-identity -n application --timeout=60s

Check which ServiceAccount the API stored:

bash
kubectl get pod default-identity -n application -o jsonpath='{.spec.serviceAccountName}{"\n"}'

Sample output:

output
default

Projected token, CA, and namespace files

The Pod also receives a projected kube-api-access-* volume. List volume names:

bash
kubectl get pod default-identity -n application -o jsonpath='{range .spec.volumes[*]}{.name}{"\n"}{end}'

Sample output:

output
kube-api-access-pv9zq

The default kube-api-access-* volume is a projected volume with three sources:

  • ServiceAccount token (bound, short-lived)
  • Cluster CA bundle from the kube-root-ca.crt ConfigMap
  • Namespace via the Downward API

Inside the container, the mount lands at the standard path:

text
/var/run/secrets/kubernetes.io/serviceaccount

List the files:

bash
kubectl exec default-identity -n application -- ls /var/run/secrets/kubernetes.io/serviceaccount/

Sample output:

output
ca.crt
namespace
token

Read the namespace file the Downward API provides:

bash
kubectl exec default-identity -n application -- cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

Sample output:

output
application

Modern clusters mount short-lived tokens through a projected volume. Kubernetes rotates them; you should not expect a permanent *-token-* Secret to appear on the ServiceAccount object automatically.

Applications that call the API read token, trust ca.crt, and use namespace to build namespaced API paths. This lesson does not decode the JWT; treat it as an opaque credential the API server validates.


Create and Assign a Custom ServiceAccount

Create a dedicated identity:

bash
kubectl create serviceaccount application-reader -n application

Assign it on the Pod spec:

yaml
spec:
  serviceAccountName: application-reader

Full Pod manifest:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-reader-pod
  namespace: application
spec:
  serviceAccountName: application-reader
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
  restartPolicy: Never

Apply the Pod manifest:

bash
kubectl apply -f app-reader-pod.yaml

Confirm the assigned identity:

bash
kubectl get pod app-reader-pod -n application -o jsonpath='{.spec.serviceAccountName}{"\n"}'

Sample output:

output
application-reader

The Pod is associated with application-reader. Any process that presents its projected token authenticates as system:serviceaccount:application:application-reader. This article does not create an RBAC binding; the token-based checks below inspect the identity and its current authorization.


Request and Verify a Short-Lived Token

For scripts and debugging outside the Pod, request a token with the API:

bash
kubectl create token application-reader -n application

Sample output (trimmed):

output
eyJhbGciOiJSUzI1NiIsImtpZCI6IjBJaFVMQjluSDJ1a0xHRXJ5NkttUzBs

You can pass --duration and --audience when you need a specific lifetime or token audience. Prefer kubectl create token over creating a long-lived Secret of type kubernetes.io/service-account-token for normal testing.

Confirm identity with kubectl auth whoami

bash
TOKEN=$(kubectl create token application-reader -n application)

When your kubeconfig already contains client certificates or a user identity, kubectl may use that identity instead of --token. Extract the API server endpoint and cluster CA from your current context, then clear the kubeconfig for these checks:

bash
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
bash
CAFILE=/tmp/k8s-ca.crt
kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > "$CAFILE"

Confirm which identity the API server sees:

bash
KUBECONFIG=/dev/null kubectl auth whoami \
  --token="$TOKEN" \
  --server="$APISERVER" \
  --certificate-authority="$CAFILE" \
  -o jsonpath='{.status.userInfo.username}{"\n"}'

Sample output:

output
system:serviceaccount:application:application-reader

kubectl auth whoami supports bearer-token authentication and returns the user attributes seen by the API server.

Test authorization with kubectl auth can-i

Then check authorization using the same token:

bash
KUBECONFIG=/dev/null kubectl auth can-i list pods \
  -n application \
  --token="$TOKEN" \
  --server="$APISERVER" \
  --certificate-authority="$CAFILE"

Sample output:

output
no

kubectl auth whoami proves that the token authenticates as system:serviceaccount:application:application-reader. The subsequent can-i request checks what that authenticated identity may do. A no result means authentication succeeded but authorization did not grant list pods.

Role and RoleBinding YAML belong in Kubernetes RBAC.


Control ServiceAccount Token Mounting

Workloads that never call the Kubernetes API should not carry API credentials. You can disable automount at the ServiceAccount or Pod level.

Disable automatic mounting

At the ServiceAccount level:

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-mount-sa
  namespace: application
automountServiceAccountToken: false

At the Pod level:

yaml
spec:
  automountServiceAccountToken: false

Pod-level versus ServiceAccount-level precedence

The Pod-level field takes precedence when both the ServiceAccount and Pod define automountServiceAccountToken.

Save both resources in no-mount-pod.yaml:

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-mount-sa
  namespace: application
automountServiceAccountToken: false
---
apiVersion: v1
kind: Pod
metadata:
  name: no-mount-pod
  namespace: application
spec:
  serviceAccountName: no-mount-sa
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
  restartPolicy: Never

Apply the manifest:

bash
kubectl apply -f no-mount-pod.yaml

Sample output:

output
serviceaccount/no-mount-sa created
pod/no-mount-pod created

Verify both the identity and absence of the projected volume:

bash
kubectl get pod no-mount-pod \
  -n application \
  -o jsonpath='serviceAccount={.spec.serviceAccountName}{"\n"}{range .spec.volumes[*]}{.name}{"\n"}{end}'

Sample output:

output
serviceAccount=no-mount-sa

An empty volume list means no kube-api-access-* volume was injected. The Pod still runs as no-mount-sa; it simply has no in-cluster API credential mounted.

Mount a custom-audience token

When you need a token at a custom path or for a specific audience, disable default automount and add your own projected volume:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: custom-audience-pod
  namespace: application
spec:
  serviceAccountName: application-reader
  automountServiceAccountToken: false
  volumes:
  - name: api-token
    projected:
      sources:
      - serviceAccountToken:
          path: token
          audience: my-app
          expirationSeconds: 3600
      - configMap:
          name: kube-root-ca.crt
          items:
          - key: ca.crt
            path: ca.crt
      - downwardAPI:
          items:
          - path: namespace
            fieldRef:
              fieldPath: metadata.namespace
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
    volumeMounts:
    - name: api-token
      mountPath: /var/run/custom-token
      readOnly: true
  restartPolicy: Never

A token with audience my-app is intended for a recipient that identifies itself as my-app. It is not automatically valid for calls to the Kubernetes API server unless that API server accepts the same audience. The default audience, when omitted, is the API server.

This token is intended for an external service that validates the my-app audience. To call the Kubernetes API, omit audience or use an audience configured in the API server's accepted audience list.

Apply the Pod and inspect the custom mount:

bash
kubectl apply -f custom-audience-pod.yaml
bash
kubectl wait pod/custom-audience-pod -n application --for=condition=Ready --timeout=120s
bash
kubectl exec custom-audience-pod -n application -- ls -1 /var/run/custom-token

Sample output:

output
ca.crt
namespace
token

The projected token is rotated by the kubelet as it approaches expiration. expirationSeconds is a requested lifetime, defaults to one hour, and must be at least 600 seconds.


Use ServiceAccounts with RBAC and imagePullSecrets

Identity and permission are separate layers:

text
ServiceAccount → RoleBinding/ClusterRoleBinding → Role/ClusterRole

Use the token-based kubectl auth whoami and kubectl auth can-i workflow from earlier to confirm identity and authorization before attaching Roles.

A ServiceAccount can also reference registry pull Secrets:

yaml
imagePullSecrets:
- name: regcred

Pods that use this ServiceAccount inherit those references when their own spec omits imagePullSecrets. The Secret must live in the same namespace as the Pod.

Creating the docker-registry Secret and troubleshooting pull failures is covered in private registry image pulls.


Choose an Account and Troubleshoot Common Failures

Requirement Choice
Pod does not call Kubernetes API Dedicated SA with automount disabled, or automountServiceAccountToken: false on the Pod
Workload requires specific API permissions Dedicated ServiceAccount with focused RBAC bindings
Quick lab with no API access Default SA may work, but disable automount in production
Registry credentials shared by many Pods Dedicated SA with imagePullSecrets

Using the namespace default ServiceAccount everywhere is convenient in labs but couples unrelated workloads to the same identity and token mount behaviour.

Symptom Likely cause Fix
Pod create returns serviceaccount "x" not found serviceAccountName is missing from the Pod namespace or misspelled Create the ServiceAccount in that namespace or correct the name
Deployment has desired replicas but no Pod object ReplicaSet Pod admission failed, possibly because the ServiceAccount is missing Run kubectl describe replicaset <name> -n <namespace> and read FailedCreate
No token file in the container automountServiceAccountToken: false on Pod or ServiceAccount Re-enable automount or add a custom projected volume when a token is required
API returns 403 Forbidden with a valid token RBAC does not grant the verb; identity succeeded Attach a RoleBinding or ClusterRoleBinding with the required permissions
Expected *-token-* Secret on ServiceAccount Legacy pattern; modern clusters use bound projected tokens Use kubectl create token or the projected mount instead
Expected ServiceAccount pull Secret is absent from the admitted Pod The Pod already defines one or more imagePullSecrets, so the ServiceAccount list was not copied Remove Pod-level imagePullSecrets when you want the ServiceAccount list copied
FailedToRetrieveImagePullSecret Secret is missing, misspelled, or not in the Pod namespace Create the Secret in the Pod namespace and match the name exactly
API calls begin returning 401 Unauthorized after ServiceAccount deletion The projected token is bound to the deleted ServiceAccount UID and is no longer valid Restore the intended ServiceAccount and recreate or verify credential refresh for affected Pods

Verify which pull Secrets the admitted Pod received:

bash
kubectl get pod <pod-name> -n application -o jsonpath='{.spec.imagePullSecrets}{"\n"}'

What's Next


References


Summary

A ServiceAccount is the API identity for a Pod. Omit serviceAccountName and you get the namespace default account; set it to a custom name and any container that presents the mounted credential authenticates as system:serviceaccount:namespace:name. Assigning a ServiceAccount associates the identity with the Pod; authentication occurs when a process actually presents the token. By default Kubernetes mounts a projected kube-api-access-* volume at /var/run/secrets/kubernetes.io/serviceaccount with a short-lived token, CA bundle, and namespace file.

Use kubectl create token when you need credentials outside the Pod. Confirm identity with kubectl auth whoami and test authorization with kubectl auth can-i, passing --token with KUBECONFIG=/dev/null so your workstation kubeconfig identity does not override the bearer token. Set automountServiceAccountToken: false on workloads that never call the API, or supply a custom projected volume when you need a specific audience or mount path. A token proves identity only—RBAC decides whether the verb is allowed.

For the request path that runs before RBAC evaluates the caller, see the authentication lesson. For Role and RoleBinding manifests, continue with the RBAC guide.


Frequently Asked Questions

1. What ServiceAccount does a Pod use when serviceAccountName is omitted?

Kubernetes assigns the namespace default ServiceAccount, normally named default. You can override it with spec.serviceAccountName on the Pod.

2. Where is the ServiceAccount token mounted in a container?

By default at /var/run/secrets/kubernetes.io/serviceaccount, through a projected kube-api-access volume that includes the token, cluster CA certificate, and namespace file.

3. How do I get a short-lived token for a ServiceAccount?

Run kubectl create token service-account-name -n namespace. Prefer this over creating a permanent token Secret for testing and automation on modern clusters.

4. How do I stop a Pod from receiving an API token?

Set automountServiceAccountToken to false on the Pod, on the ServiceAccount, or both. The Pod-level setting takes precedence when both are set.

5. Does a ServiceAccount token grant API permissions by itself?

No. The token authenticates the workload as system:serviceaccount:namespace:name. RBAC RoleBindings or ClusterRoleBindings determine what API verbs are allowed.

6. Can imagePullSecrets on a ServiceAccount replace Pod imagePullSecrets?

Pods using that ServiceAccount inherit its imagePullSecrets when the Pod spec does not define its own list. The Secret must exist in the same namespace.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)