ServiceAccounts are Kubernetes-native identities for workloads (Pods, Jobs, controllers). They provide the mechanism for Pods to authenticate with the API server and external services. Understanding how to create, assign, and secure them is fundamental for both CKA and CKS.

1. ServiceAccount Basics

# Create a ServiceAccount:
kubectl create serviceaccount app-sa -n production

# Or declaratively:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
  annotations:
    # AWS: associate with IAM role (IRSA):
    eks.amazonaws.com/role-arn: arn:aws:iam::123456:role/app-role
    # GCP: Workload Identity:
    iam.gke.io/gcp-service-account: app@project.iam.gserviceaccount.com

The Default ServiceAccount

Every namespace automatically gets a default ServiceAccount. Every Pod without an explicit serviceAccountName uses it.

kubectl get sa -n default
# NAME      SECRETS   AGE
# default   0         30d

# Pod without explicit SA:
spec:
  # serviceAccountName: default  ← implicit (auto-assigned)
The default SA has no special permissions (in properly secured clusters). It authenticates as system:serviceaccount:<namespace>:default, but unless RBAC grants it permissions, it can't do anything with the API server. However, it still gets a token mounted — which is unnecessary for Pods that don't talk to the API.

Assigning a ServiceAccount to a Pod

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  serviceAccountName: app-sa        # ← Explicit SA assignment
  containers:
    - name: app
      image: myapp:latest
# The Pod's identity becomes:
# user: system:serviceaccount:production:app-sa
# groups: [system:serviceaccounts, system:serviceaccounts:production]

# This identity is used for RBAC authorization

2. Token Projection (How Pods Get Tokens)

Since K8s 1.22, Pods receive bound projected tokens — short-lived JWTs mounted automatically:

# What kubelet mounts into every Pod:
/var/run/secrets/kubernetes.io/serviceaccount/
├── token        # ← Bound JWT (rotated every ~1h by kubelet)
├── ca.crt       # ← Cluster CA (to verify API server)
└── namespace    # ← Pod's namespace (text file)

Token Properties

PropertyBound Token (Modern)Legacy Token (Pre-1.22)
Expiry~1 hour (auto-rotated)Never expires
Bound to Pod✅ (invalidated on Pod delete)❌ (valid forever even after Pod gone)
AudienceRestricted (API server only by default)No audience restriction
StorageProjected volume (not in a Secret)Stored in a Secret object
RotationAutomatic (kubelet renews before expiry)Manual (delete Secret to regenerate)

Custom Token Projection

# Request a token with custom audience and expiry:
spec:
  containers:
    - name: app
      volumeMounts:
        - name: vault-token
          mountPath: /var/run/secrets/vault
  volumes:
    - name: vault-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 7200       # 2 hours
              audience: vault               # Custom audience (for Vault auth)
Custom audiences enable workload identity federation. A token with audience: vault is only valid when presented to Vault — not the API server. This is how IRSA (AWS), Workload Identity (GCP), and Vault's K8s auth method work: they validate the token's audience claim to ensure it's intended for them, not stolen from an API server context.

Verifying Token Content

# Read the token inside a Pod:
kubectl exec web -- cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Decode the JWT (it's base64-encoded sections):
kubectl exec web -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
  cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
# {
#   "aud": ["https://kubernetes.default.svc"],
#   "exp": 1705334400,
#   "sub": "system:serviceaccount:production:app-sa",
#   "kubernetes.io": {
#     "pod": {"name": "web", "uid": "..."},
#     "serviceaccount": {"name": "app-sa"}
#   }
# }

3. Disabling Auto-Mount

Most Pods don't need to talk to the Kubernetes API. Mounting a token unnecessarily increases the attack surface — a compromised Pod gains API access for free.

Disable at the Pod Level

spec:
  automountServiceAccountToken: false    # No token mounted into this Pod
  containers:
    - name: app
      image: myapp:latest
# Result: /var/run/secrets/kubernetes.io/serviceaccount/ doesn't exist

Disable at the ServiceAccount Level

apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-api-access
automountServiceAccountToken: false     # No Pod using this SA gets a token
# Unless the Pod explicitly overrides with automountServiceAccountToken: true

Priority

SA SettingPod SettingResult
true (default)Not setToken mounted
truefalseNo token (Pod wins)
falseNot setNo token (SA wins)
falsetrueToken mounted (Pod overrides)
CKS best practice: disable auto-mount by default. Create a ServiceAccount with automountServiceAccountToken: false for most workloads. Only enable it for Pods that genuinely need API access (operators, controllers, monitoring agents). This limits the blast radius of container compromise.

4. Security Best Practices

PracticeWhy
One SA per workloadPrinciple of least privilege — don't share SA across unrelated apps
Disable auto-mount for most PodsMost Pods don't need API access — don't give them a token
Use RBAC to limit SA permissionsEven if token is stolen, attacker can only do what RBAC allows
Never use the default SA for real workloadsThe default SA is shared — any RBAC grant affects all Pods in the namespace
Prefer bound tokens over staticShort-lived, Pod-bound tokens minimize damage window if leaked
Use audience-bound tokens for external servicesToken is useless outside its intended audience

Cloud Workload Identity

# AWS IRSA (IAM Roles for Service Accounts):
# Annotate SA → Pod gets AWS credentials via projected token
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456:role/s3-reader
# Pod can assume the IAM role using its SA token — no AWS keys in cluster!

# GCP Workload Identity:
metadata:
  annotations:
    iam.gke.io/gcp-service-account: reader@project.iam.gserviceaccount.com
# Pod authenticates to GCP APIs using its SA token
Cloud workload identity (IRSA, Workload Identity, Azure Workload Identity) is the modern way to give Pods access to cloud resources. Instead of storing AWS keys as Secrets, the Pod uses its K8s SA token to federate into a cloud IAM role. No static credentials in the cluster, automatic rotation, audience-bound. Always use this over Secret-stored cloud keys.

Summary

ConceptKey Point
ServiceAccountK8s-native identity for workloads (namespaced object)
Default SAAuto-created per namespace. Has no permissions by default. Don't use for real workloads.
Pod identitysystem:serviceaccount:<ns>:<name>
Bound tokensShort-lived (1h), Pod-bound, audience-restricted, auto-rotated
Token path/var/run/secrets/kubernetes.io/serviceaccount/token
automountServiceAccountTokenSet false to prevent token mount (Pod or SA level)
Custom audienceProjected token with audience: vault — valid only for that service
Cloud workload identityIRSA/Workload Identity: SA annotation → cloud IAM role (no static keys)
Least privilegeOne SA per workload, disable auto-mount, RBAC-limit each SA

📝 Quiz: ServiceAccounts

Q1: A Pod doesn't specify serviceAccountName. What SA does it use and what identity does the API server see?

It uses the default ServiceAccount in its namespace. The API server sees it as user: system:serviceaccount:<namespace>:default, groups: [system:serviceaccounts, system:serviceaccounts:<namespace>].

Q2: You set automountServiceAccountToken: false on a ServiceAccount. A Pod using that SA explicitly sets automountServiceAccountToken: true in its spec. Is the token mounted?

Yes. The Pod-level setting overrides the SA-level setting. The Pod explicitly requests a token mount, which takes precedence. The SA's false is the default for Pods that don't specify — but when a Pod explicitly says true, it wins.

Q3: A bound SA token expires in 1 hour. What happens to the Pod when it expires?

Nothing disrupts the Pod. The kubelet automatically rotates the token before it expires — it requests a new token and updates the file at the mount path. Client libraries (client-go, official SDKs) re-read the token file periodically. The Pod continues operating seamlessly. Expiry only matters if the Pod is deleted (token becomes permanently invalid).

Q4: Why should you NOT use the default ServiceAccount for production workloads?

Because the default SA is shared by all Pods in the namespace that don't specify a SA. Any RBAC RoleBinding you create for the default SA applies to ALL those Pods. If you grant it Secret-read access for one Pod, every Pod in the namespace inherits that permission. Use dedicated SAs per workload for isolation.

Q5: How does AWS IRSA (IAM Roles for ServiceAccounts) work at a high level?

(1) SA is annotated with an IAM role ARN. (2) The Pod gets a projected token with audience=sts.amazonaws.com. (3) The AWS SDK in the Pod exchanges this K8s token for temporary AWS credentials via STS AssumeRoleWithWebIdentity. (4) AWS validates the token signature against the cluster's OIDC endpoint. (5) Pod gets temporary IAM credentials — no static keys stored anywhere in K8s.

Q6: A Pod's token has audience: ["vault"]. The Pod tries to use it to call the Kubernetes API server. What happens?

Authentication fails (401 Unauthorized). The API server checks the token's audience claim. The API server expects tokens with its own audience (https://kubernetes.default.svc). A token with audience: vault is not valid for the API server — it's only valid when presented to Vault. This audience restriction is a key security feature of bound tokens.