KCSA — Service Accounts & Token Security

6 minute read

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

  1. All pods share the same identity — if one pod is compromised, the attacker inherits the default SA’s permissions.
  2. A token is mounted automatically — even if the pod never needs API access.
  3. Permissions accumulate — if anyone binds RBAC roles to the default SA, 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

  1. Attacker compromises a pod in namespace dev.
  2. The pod has a mounted service account token.
  3. The service account has a ClusterRoleBinding granting permissions in namespace production.
  4. The attacker uses the token to access resources in production.

Another Scenario — Token Exfiltration

  1. A legacy (non-expiring) service account token is stored in a Secret.
  2. A user with get secrets permission in the namespace reads the token.
  3. The user uses the token outside the cluster to authenticate as the service account.
  4. 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.

User & Group Management

Updated: