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)
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
| Property | Bound 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) |
| Audience | Restricted (API server only by default) | No audience restriction |
| Storage | Projected volume (not in a Secret) | Stored in a Secret object |
| Rotation | Automatic (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)
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 Setting | Pod Setting | Result |
|---|---|---|
true (default) | Not set | Token mounted |
true | false | No token (Pod wins) |
false | Not set | No token (SA wins) |
false | true | Token mounted (Pod overrides) |
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
| Practice | Why |
|---|---|
| One SA per workload | Principle of least privilege — don't share SA across unrelated apps |
| Disable auto-mount for most Pods | Most Pods don't need API access — don't give them a token |
| Use RBAC to limit SA permissions | Even if token is stolen, attacker can only do what RBAC allows |
Never use the default SA for real workloads | The default SA is shared — any RBAC grant affects all Pods in the namespace |
| Prefer bound tokens over static | Short-lived, Pod-bound tokens minimize damage window if leaked |
| Use audience-bound tokens for external services | Token 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
Summary
| Concept | Key Point |
|---|---|
| ServiceAccount | K8s-native identity for workloads (namespaced object) |
| Default SA | Auto-created per namespace. Has no permissions by default. Don't use for real workloads. |
| Pod identity | system:serviceaccount:<ns>:<name> |
| Bound tokens | Short-lived (1h), Pod-bound, audience-restricted, auto-rotated |
| Token path | /var/run/secrets/kubernetes.io/serviceaccount/token |
| automountServiceAccountToken | Set false to prevent token mount (Pod or SA level) |
| Custom audience | Projected token with audience: vault — valid only for that service |
| Cloud workload identity | IRSA/Workload Identity: SA annotation → cloud IAM role (no static keys) |
| Least privilege | One 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?
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?
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?
Q4: Why should you NOT use the default ServiceAccount for production workloads?
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?
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?
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.