KCSA — Service Accounts & Token Security
Service Accounts — Pod Identity in Kubernetes
Service accounts provide an identity for processes running inside pods. Unlike human users (who authenticate via certificates or OIDC), pods authenticate to the API server using service account tokens.
Every namespace has a default service account that is automatically assigned to pods unless a different one is specified — and this default is one of the biggest security risks in Kubernetes clusters.
KCSA Relevance: Service account token security is a frequently tested topic. Understand the difference between legacy and projected tokens, and know when to disable auto-mounting.
Service Accounts vs User Accounts
| Aspect | User Accounts | Service Accounts |
|---|---|---|
| Who uses them | Humans (developers, admins) | Processes (pods, controllers) |
| Managed by | External (IdP, certificates) | Kubernetes API (namespaced objects) |
| Kubernetes API object | No — no User resource exists |
Yes — ServiceAccount resource |
| Namespace-scoped | N/A | Yes |
| Authentication method | Certs, OIDC, webhook | JWT tokens |
| Created automatically | No | Yes — default SA in every namespace |
Creating a Service Account
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: production
kubectl create serviceaccount my-app-sa -n production
Assigning a Service Account to a Pod
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: production
spec:
serviceAccountName: my-app-sa
containers:
- name: app
image: my-app:1.0
The API server identifies this pod as system:serviceaccount:production:my-app-sa.
The Default Service Account Problem
Every namespace is created with a default service account. If a pod does not specify serviceAccountName, it uses default.
Why This Is a Security Risk
- All pods share the same identity — if one pod is compromised, the attacker inherits the
defaultSA’s permissions. - A token is mounted automatically — even if the pod never needs API access.
- Permissions accumulate — if anyone binds RBAC roles to the
defaultSA, every pod in the namespace gains those permissions.
Never bind RBAC roles to the default service account. Create dedicated service accounts for each workload and grant only the permissions each workload needs.
Disabling Automatic Token Mounting
If a pod does not need to communicate with the Kubernetes API, disable the token mount entirely.
At the Pod Level
apiVersion: v1
kind: Pod
metadata:
name: static-web
spec:
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.25
At the Service Account Level
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-api-access
namespace: default
automountServiceAccountToken: false
When set at the ServiceAccount level, all pods using that SA will have token mounting disabled unless the pod explicitly overrides it with automountServiceAccountToken: true.
Exam Tip: Setting automountServiceAccountToken: false is a best practice for any pod that does not need to call the Kubernetes API. This reduces the attack surface if the pod is compromised.
Legacy vs Projected Service Account Tokens
Kubernetes has moved from legacy (Secret-based) tokens to projected (volume-based) tokens. Understanding this transition is critical for the KCSA.
Comparison Table
| Feature | Legacy Tokens | Projected Tokens |
|---|---|---|
| Expiration | Never expires | Configurable (default: 1 hour, auto-refreshed) |
| Audience | None (valid for any audience) | Bound to specific audience |
| Storage | Stored as a Secret object | Mounted via projected volume |
| Rotation | Manual — delete and recreate Secret | Automatic — kubelet refreshes before expiry |
| Revocation | Delete the Secret | Token expires naturally |
| Identity binding | Bound to SA only | Bound to SA + pod + expiry + audience |
| Available since | Kubernetes 1.0 | Kubernetes 1.20+ (stable in 1.22+) |
Legacy Token Example (Deprecated Pattern)
In older Kubernetes versions, creating a ServiceAccount automatically created a corresponding Secret:
# This pattern is DEPRECATED
apiVersion: v1
kind: Secret
metadata:
name: my-app-sa-token-xxxxx
annotations:
kubernetes.io/service-account.name: my-app-sa
type: kubernetes.io/service-account-token
The token in this Secret never expires. If it is leaked, an attacker has permanent access until the Secret is manually deleted.
Projected Token (Current Default)
Starting with Kubernetes 1.22+, pods receive projected tokens by default:
# This is what Kubernetes mounts automatically
volumes:
- name: kube-api-access
projected:
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
fieldPath: metadata.namespace
The token is:
- Time-bound — expires after the configured duration.
- Audience-bound — only valid for the intended API server.
- Pod-bound — invalidated if the pod is deleted.
- Auto-refreshed — the kubelet requests a new token before expiration.
BoundServiceAccountTokenVolume
The BoundServiceAccountTokenVolume feature (stable since Kubernetes 1.22) changed the default token mounting behavior:
| Behavior | Before 1.22 | After 1.22 |
|---|---|---|
| Token source | Secret object | Projected volume |
| Token lifetime | Infinite | 1 hour (auto-refreshed) |
| Token audience | Unbound | https://kubernetes.default.svc |
| Deleted with pod | No (Secret persists) | Yes (bound to pod lifecycle) |
This was a major security improvement — pods no longer receive permanent credentials by default.
Token Request API
The TokenRequest API allows fine-grained control over service account token issuance.
# Request a token with a specific audience and expiration
kubectl create token my-app-sa \
--namespace production \
--audience api.example.com \
--duration 30m
TokenRequest API Properties
| Property | Description |
|---|---|
audience |
Intended audience — token is only valid for this audience |
expirationSeconds |
Token lifetime (minimum: 10 minutes) |
boundObjectRef |
Bind token to a specific pod or Secret |
When to Use TokenRequest API
- Pods that need tokens for external services (not the Kubernetes API).
- Short-lived tokens for CI/CD pipelines.
- Tokens with custom audiences for service mesh or vault integration.
Cross-Namespace Service Account Abuse
Service account tokens can be used from anywhere, not just within the originating namespace. This creates attack vectors:
Attack Scenario
- Attacker compromises a pod in namespace
dev. - The pod has a mounted service account token.
- The service account has a ClusterRoleBinding granting permissions in namespace
production. - The attacker uses the token to access resources in
production.
Another Scenario — Token Exfiltration
- A legacy (non-expiring) service account token is stored in a Secret.
- A user with
get secretspermission in the namespace reads the token. - The user uses the token outside the cluster to authenticate as the service account.
- This bypasses any network-level security controls.
Mitigations
| Mitigation | How It Helps |
|---|---|
| Use projected tokens | Tokens expire and are bound to pods |
| Disable auto-mount | No token available to exfiltrate |
| Namespace-scoped Roles only | Limits blast radius of compromised SA |
| Avoid ClusterRoleBindings for SAs | Prevents cross-namespace access |
| Audit RBAC regularly | Detect over-permissioned SAs early |
Best Practices Summary
| Practice | Reason |
|---|---|
| Create dedicated SAs per workload | Isolates identity and permissions |
Set automountServiceAccountToken: false |
Reduces attack surface for pods that don’t need API access |
| Use projected tokens (default in 1.22+) | Time-bound, audience-bound, pod-bound |
Never bind roles to default SA |
Prevents unintended permission inheritance |
| Use TokenRequest API for external integrations | Custom audience, controlled expiration |
| Audit SA permissions regularly | kubectl auth can-i --list --as system:serviceaccount:ns:name |
| Delete unused SAs and their Secrets | Reduces credential sprawl |
| Apply least privilege | Only grant SAs the exact permissions they need |
Verifying Service Account Configuration
# List service accounts in a namespace
kubectl get serviceaccounts -n production
# Check what a service account can do
kubectl auth can-i --list \
--as system:serviceaccount:production:my-app-sa
# Check if a pod has a token mounted
kubectl exec my-app -n production -- \
ls /var/run/secrets/kubernetes.io/serviceaccount/
# Decode and inspect a projected token (JWT)
kubectl exec my-app -n production -- \
cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
cut -d. -f2 | base64 -d | jq .
Key Takeaways for the Exam
| Concept | What to Remember |
|---|---|
| Service accounts are namespace-scoped | Identified as system:serviceaccount:<ns>:<name> |
default SA is auto-assigned |
Security risk — never bind roles to it |
automountServiceAccountToken: false |
Disable when API access is not needed |
| Legacy tokens | Stored in Secrets, never expire — deprecated |
| Projected tokens | Time-bound, audience-bound, pod-bound — current default |
| TokenRequest API | On-demand tokens with custom audience and expiration |
| Cross-namespace abuse | Tokens work anywhere — scope RBAC to namespaces |
| Kubernetes 1.22+ | Switched to projected tokens by default |
What’s Next?
Now that you understand how pods are identified and authenticated, the next topic covers how human users and groups are managed in Kubernetes.