Skip to main content

Service Account Token Security

Required knowledge for the CKS certification.

Service account tokens provide pods with authentication credentials for accessing the Kubernetes API server. Misconfigured tokens with excessive permissions, long expiration times, or automatic mounting create significant security risks including privilege escalation and lateral movement.

Issue: Service account tokens are automatically mounted into every pod by default, granting unnecessary API access that can be exploited if containers are compromised.
Fix: Disable automatic token mounting, use bound tokens with short expiration, and apply strict RBAC controls to limit service account permissions.


1. Disable Automatic Service Account Token Mounting

Issue: Every pod automatically mounts a service account token, even if it never needs to access the Kubernetes API.
Fix: Disable automountServiceAccountToken at both ServiceAccount and Pod levels.

Disable at ServiceAccount Level

apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
automountServiceAccountToken: false

Disable at Pod Level

apiVersion: v1
kind: Pod
metadata:
name: frontend-app
namespace: production
spec:
serviceAccountName: app-service-account
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.21

Set Default Deny Policy

Create a default service account that denies token mounting:

apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
automountServiceAccountToken: false

2. Use Bound Service Account Tokens

Issue: Legacy service account tokens are long-lived (non-expiring) and can be used from anywhere, even outside the cluster.
Fix: Use bound service account tokens introduced in Kubernetes 1.21+ with audience, expiration, and bound to pod lifetime.

Configure Token Request Projection

apiVersion: v1
kind: Pod
metadata:
name: api-client
namespace: production
spec:
serviceAccountName: api-service-account
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: api.company.com

Verify Token is Bound

# Inside the pod, check token properties
TOKEN=$(cat /var/run/secrets/tokens/token)

# Decode JWT to verify expiration and audience
echo $TOKEN | cut -d '.' -f 2 | base64 -d | jq .

Output shows:

{
"aud": ["api.company.com"],
"exp": 1702345600,
"iat": 1702342000,
"iss": "https://kubernetes.default.svc.cluster.local",
"kubernetes.io": {
"namespace": "production",
"pod": {
"name": "api-client",
"uid": "abc-123"
},
"serviceaccount": {
"name": "api-service-account",
"uid": "def-456"
}
}
}

Set Short Expiration Times

apiVersion: v1
kind: Pod
metadata:
name: batch-job
namespace: production
spec:
serviceAccountName: batch-sa
containers:
- name: worker
image: worker:1.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 600 # 10 minutes

3. Apply Least Privilege RBAC

Issue: Service accounts often have overly broad permissions granting access to resources they don't need.
Fix: Apply principle of least privilege and grant only the minimum required permissions.

Create Minimal Role

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
resourceNames: ["app-config"] # Restrict to specific ConfigMap
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-configmap-reader
namespace: production
subjects:
- kind: ServiceAccount
name: app-service-account
namespace: production
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io

Deny Dangerous Permissions

Never grant these permissions to application service accounts:

# DANGEROUS - DO NOT USE
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dangerous-role
namespace: production
rules:
# NEVER allow secrets access
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]

# NEVER allow pod creation
- apiGroups: [""]
resources: ["pods"]
verbs: ["create"]

# NEVER allow serviceaccounts/token
- apiGroups: [""]
resources: ["serviceaccounts/token"]
verbs: ["create"]

Audit Service Account Permissions

# List all service accounts with their roles
kubectl get rolebindings,clusterrolebindings -A -o json | \
jq '.items[] | select(.subjects[]?.kind == "ServiceAccount")'

# Check specific service account permissions
kubectl auth can-i --list --as=system:serviceaccount:production:app-sa

# Find service accounts with secrets access
kubectl get roles,clusterroles -A -o json | \
jq '.items[] | select(.rules[]? | select(.resources[]? == "secrets" and (.verbs[]? == "get" or .verbs[]? == "*")))'

4. Disable Legacy Service Account Token Secrets

Issue: Pre-1.24 clusters automatically create non-expiring token secrets for each service account.
Fix: Disable automatic token secret creation and clean up existing legacy tokens.

Disable LegacyServiceAccountTokenNoAutoGeneration

# Add to kube-apiserver flags
kube-apiserver \
--feature-gates=LegacyServiceAccountTokenNoAutoGeneration=true

Delete Existing Legacy Tokens

# List all legacy token secrets
kubectl get secrets -A -o json | \
jq -r '.items[] | select(.type=="kubernetes.io/service-account-token") | "\(.metadata.namespace) \(.metadata.name)"'

# Delete legacy tokens (be careful!)
kubectl delete secret -n production default-token-xxxxx

Verify No Legacy Tokens Remain

# Check service accounts have no token secrets
kubectl get sa app-sa -n production -o jsonpath='{.secrets}'

# Should return empty: []

5. Implement Token Request API Controls

Issue: The TokenRequest API can be abused to generate tokens for any service account if permissions are too broad.
Fix: Restrict access to the serviceaccounts/token subresource.

Deny TokenRequest Access by Default

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: no-token-request
rules:
# Explicitly deny token creation
- apiGroups: [""]
resources: ["serviceaccounts/token"]
verbs: [] # Empty verbs = no access

Audit TokenRequest Usage

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
verbs: ["create"]
resources:
- group: ""
resources: ["serviceaccounts/token"]
omitStages:
- RequestReceived

Monitor Token Creation

Using Falco:

- rule: Suspicious Token Request
desc: Detect TokenRequest API abuse
condition: >
kevt and ka.verb = create and
ka.target.resource = serviceaccounts/token and
ka.target.subresource = token
output: >
TokenRequest API called (user=%ka.user.name
target_sa=%ka.target.name ns=%ka.target.namespace)
priority: WARNING
tags: [k8s, token, privilege_escalation]

6. Use Workload Identity Federation

Issue: Service account tokens provide cluster API access when applications only need external service authentication.
Fix: Use workload identity to authenticate to external services without cluster API access.

AWS EKS with IRSA

apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-access-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/S3AccessRole
automountServiceAccountToken: false # No cluster API access needed
---
apiVersion: v1
kind: Pod
metadata:
name: s3-app
namespace: production
spec:
serviceAccountName: s3-access-sa
containers:
- name: app
image: myapp:1.0
env:
- name: AWS_ROLE_ARN
value: arn:aws:iam::123456789012:role/S3AccessRole
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

GKE with Workload Identity

apiVersion: v1
kind: ServiceAccount
metadata:
name: gcs-access-sa
namespace: production
annotations:
iam.gke.io/gcp-service-account: gcs-accessor@project.iam.gserviceaccount.com
automountServiceAccountToken: false
---
apiVersion: v1
kind: Pod
metadata:
name: gcs-app
namespace: production
spec:
serviceAccountName: gcs-access-sa
containers:
- name: app
image: myapp:1.0

Bind the GCP service account:

gcloud iam service-accounts add-iam-policy-binding \
gcs-accessor@project.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:project.svc.id.goog[production/gcs-access-sa]"

Azure AKS with Managed Identity

apiVersion: v1
kind: ServiceAccount
metadata:
name: storage-access-sa
namespace: production
annotations:
azure.workload.identity/client-id: "12345678-1234-1234-1234-123456789012"
automountServiceAccountToken: false
---
apiVersion: v1
kind: Pod
metadata:
name: storage-app
namespace: production
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: storage-access-sa
containers:
- name: app
image: myapp:1.0

7. Implement Pod Security Admission

Issue: Pods can be created with service accounts that have excessive privileges without validation.
Fix: Use Pod Security Admission to enforce controls on service account usage.

Enforce Restricted Profile

apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted

Create Admission Policy for Service Accounts

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-token-disabled
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.spec.automountServiceAccountToken == false"
message: "Pods must explicitly disable service account token mounting"

8. Monitor and Audit Token Usage

Issue: Service account token abuse goes undetected without proper monitoring.
Fix: Implement comprehensive audit logging and alerting for token-related activity.

Enable API Server Audit Logging

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
verbs: ["create"]
resources:
- group: ""
resources: ["serviceaccounts", "serviceaccounts/token"]
- level: Metadata
verbs: ["impersonate"]
resources:
- group: ""
resources: ["serviceaccounts"]

Create Alerts for Suspicious Activity

Using Prometheus + AlertManager:

- alert: ServiceAccountTokenCreated
expr: |
increase(apiserver_audit_event_total{
verb="create",
objectRef_resource="serviceaccounts",
objectRef_subresource="token"
}[5m]) > 0
labels:
severity: warning
annotations:
summary: "Service account token created via API"

Query Token Usage Patterns

# Find pods with mounted tokens
kubectl get pods -A -o json | \
jq '.items[] | select(.spec.automountServiceAccountToken != false)'

# Check API audit logs for token usage
kubectl logs -n kube-system kube-apiserver-master | \
grep "serviceaccounts/token"

Security Checklist

  • Disable automountServiceAccountToken by default for all service accounts
  • Use bound service account tokens with short expiration (< 1 hour)
  • Apply least privilege RBAC to all service accounts
  • Remove permissions for secrets, pods, and serviceaccounts/token
  • Disable legacy service account token secret creation
  • Delete existing non-expiring token secrets
  • Use workload identity federation for external service authentication
  • Implement Pod Security Admission with restricted profile
  • Enable comprehensive audit logging for token-related operations
  • Monitor TokenRequest API usage with alerts
  • Regularly audit service account permissions
  • Document which service accounts require API access and why