Skip to main content

Prefer Bound Projected ServiceAccount Tokens Over Secret

More Info:​

Advisory: avoid long-lived ServiceAccount token Secrets; use projected (TokenRequest) tokens with an audience and expiry instead.

Risk Level​

Informational

Address​

Security

Compliance Standards​

  • Cloudanix Best Practice

Triage and Remediation​

Remediation​

Manual Steps
  1. List all ServiceAccount token Secrets

    • Run on: any machine with kubectl access
    • Command:
      kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token -o wide
    • Review which namespaces and ServiceAccounts still have token Secrets and note any in application namespaces (not just kube-system).
  2. Identify who uses each token Secret and how

    • For a specific Secret, inspect it (do not paste token contents into tickets/logs):
      kubectl describe secret -n <namespace> <secret-name>
    • Search for references to that Secret name in:
      • Pod specs:
        kubectl get pods -n <namespace> -o yaml | grep -C3 "<secret-name>" || true
      • Deployments/StatefulSets/DaemonSets/Jobs/CronJobs:
        kubectl get deploy,sts,ds,job,cronjob -n <namespace> -o yaml | grep -C3 "<secret-name>" || true
    • Also search your app/IaC repos for the Secret name or service-account-token usage.
  3. Decide whether the workload can switch to projected ServiceAccount tokens
    For each workload that uses a ServiceAccount token Secret:

    • Confirm if it really needs to call the Kubernetes API from inside the pod. If not, plan to remove the token mount entirely.
    • If it does:
      • Check if the code or sidecar can read the token from:
        • The default projected token mount (/var/run/secrets/kubernetes.io/serviceaccount/token), or
        • An explicit projected ServiceAccount token volume with audience and expirationSeconds.
      • If the current configuration is hard‑wiring a Secret volume/volumeMount, plan to migrate it to a projected ServiceAccount token volume.
  4. Reconfigure manifests to stop using long‑lived token Secrets

    • For each affected ServiceAccount:
      • If the only reason it exists is to auto‑provision a legacy token Secret, ensure there are no pods mounting that Secret, then plan to delete the Secret.
    • For each affected workload manifest (e.g., Deployment):
      • Remove explicit secret volume entries and volumeMounts that point at kubernetes.io/service-account-token Secrets.
      • Ensure spec.serviceAccountName is set to the intended ServiceAccount.
      • If the app cannot use the default mount, define a projected token volume similar to:
        volumes:
        - name: sa-token
        projected:
        sources:
        - serviceAccountToken:
        path: token
        audience: "<audience-for-this-app>"
        expirationSeconds: 3600
        volumeMounts:
        - name: sa-token
        mountPath: /var/run/myapp
        readOnly: true
    • Apply changes:
      kubectl apply -f <updated-manifest>.yaml
  5. Remove unused ServiceAccount token Secrets after migration

    • For each previously identified Secret, confirm no pod is mounting it:
      kubectl get pods -n <namespace> -o yaml | grep -C3 "<secret-name>" || true
    • When you are sure it is unused, delete it:
      kubectl delete secret -n <namespace> <secret-name>
    • In AKS, new long‑lived ServiceAccount token Secrets should not be auto‑created by recent Kubernetes versions; if you see new ones appear, investigate controllers or tools that may be creating them manually.
  6. Verify that only projected tokens are in use

    • Re-run the review command and confirm that no application namespaces rely on kubernetes.io/service-account-token Secrets:
      kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token -o wide
    • Spot‑check updated pods to ensure they are using projected tokens (either the default mount or your projected volume) and that no secret‑type volumes for ServiceAccount tokens remain.
Using kubectl
# 1. List all long-lived ServiceAccount token Secrets
# Run on: any machine with kubectl access
kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token \
-o wide

What to look for (potential problems):

  • Many Secrets of type kubernetes.io/service-account-token in namespaces that host apps using modern SDKs or that already use projected tokens.
  • Namespaces where workloads are short‑lived or highly sensitive but still have multiple legacy token Secrets.
  • ServiceAccounts used by critical apps that also have a corresponding token Secret and no sign of projected token use in the Pods.

# 2. For a specific Secret, inspect annotations and age
# Replace NAMESPACE and SECRET_NAME with values from the previous output
kubectl get secret SECRET_NAME -n NAMESPACE -o yaml

What to look for:

  • type: kubernetes.io/service-account-token
  • Very old metadata.creationTimestamp (indicates a long‑lived token).
  • Annotation kubernetes.io/service-account.name pointing to a ServiceAccount still in use.
  • No automated process rotating or deleting these Secrets.

# 3. Identify whether active Pods mount these Secrets directly
kubectl get pods -A -o jsonpath='{range .items[*]}{@.metadata.namespace}{" "}{@.metadata.name}{"\n"}{range @.spec.volumes[*]}{" "}{@.name}{" => "}{@.secret.secretName}{"\n"}{end}{"\n"}{end}' 2>/dev/null | grep -v " => <no value>"

What to look for:

  • Pods that reference Secrets of type kubernetes.io/service-account-token as volumes.
  • These indicate applications may be using long‑lived token Secrets rather than projected tokens.

# 4. For a specific Pod, confirm whether it uses projected ServiceAccount tokens
kubectl get pod POD_NAME -n NAMESPACE -o yaml

What to look for (good vs. problematic):

  • Good (preferred):
    • Volumes of type projected with a serviceAccountToken source:
      volumes:
      - name: sa-token
      projected:
      sources:
      - serviceAccountToken:
      path: token
      audience: <some-audience>
      expirationSeconds: <short duration>
  • Problematic (legacy):
    • Volumes of type secret that reference a ServiceAccount token Secret:
      volumes:
      - name: sa-token
      secret:
      secretName: <serviceaccount-name>-token-xxxxx
    • Containers reading the token from /var/run/secrets/kubernetes.io/serviceaccount/token directly with no projected token volume.

# 5. Inspect the ServiceAccount to see how tokens are being used
kubectl get serviceaccount SA_NAME -n NAMESPACE -o yaml

What to look for:

  • secrets: list containing entries like <serviceaccount-name>-token-xxxxx (indicates classic token Secret exists).
  • Whether Pods using this ServiceAccount (from step 4) mount that Secret directly instead of using a projected serviceAccountToken volume.

# 6. (AKS-specific context) Check for legacy ServiceAccount token use across a namespace
# Helpful to focus review on application namespaces
kubectl get secrets -n NAMESPACE --field-selector type=kubernetes.io/service-account-token \
-o jsonpath='{range .items[*]}{@.metadata.name}{" "}{@.metadata.creationTimestamp}{"\n"}{end}'

What to look for:

  • Very old tokens in application namespaces still attached to active ServiceAccounts.
  • Patterns where every ServiceAccount automatically gets and keeps a token Secret, while Pods are not using projected volumes. This suggests refactoring to TokenRequest/projected tokens is needed.

These kubectl commands only surface current usage; a human must decide per application whether and how to migrate from long‑lived ServiceAccount token Secrets to projected tokens with audience and expiry.

Automation
#!/usr/bin/env bash
# Report legacy ServiceAccount token Secrets and pods using projected tokens in an AKS cluster.
# Run on: any machine with kubectl access and current context set to the AKS cluster.

set -euo pipefail

echo "=== 1) Legacy ServiceAccount token Secrets (type=kubernetes.io/service-account-token) ==="
echo
kubectl get secrets -A \
--field-selector type=kubernetes.io/service-account-token \
-o json | jq -r '
.items[]? |
[
.metadata.namespace,
.metadata.name,
.metadata.annotations."kubernetes.io/service-account.name",
.metadata.annotations."kubernetes.io/service-account.uid",
(.metadata.creationTimestamp // "unknown")
] | @tsv
' | column -t || echo "No legacy ServiceAccount token Secrets found."

cat <<'EOF'

Interpretation:
- Any row above represents a long-lived ServiceAccount token Secret.
- These are what you should be avoiding in favor of projected (TokenRequest) tokens.
- Pay special attention to:
* Very old creationTimestamp values (indicates long-lived tokens).
* ServiceAccounts that should not need cluster access, or have broad RBAC.

EOF

echo "=== 2) Pods using projected ServiceAccount tokens (serviceAccountToken projection) ==="
echo
kubectl get pods -A -o json | jq -r '
.items[]
| . as $pod
| [
$pod.metadata.namespace,
$pod.metadata.name,
(
[$pod.spec.volumes[]?
| select(.projected?.sources[]? .serviceAccountToken != null)
| .name
] | unique | join(",")
)
]
| select(.[2] != "")
| @tsv
' | column -t || echo "No pods with projected ServiceAccountToken volumes found."

cat <<'EOF'

Interpretation:
- Rows above show pods that are already using projected ServiceAccount tokens.
- Namespaces/pods NOT listed here but that use ServiceAccounts likely still rely on:
* The legacy auto-mounted Secret token volume, or
* Out-of-cluster storage of long-lived ServiceAccount tokens.

EOF

echo "=== 3) ServiceAccounts with legacy token auto-mount still enabled (serviceAccountToken secrets present) ==="
echo
kubectl get serviceaccounts -A -o json | jq -r '
.items[]
| . as $sa
| [
$sa.metadata.namespace,
$sa.metadata.name,
(
[$sa.secrets[]?.name] | join(",")
)
]
| select(.[2] != "")
| @tsv
' | column -t || echo "No ServiceAccounts with attached Secrets recorded."

cat <<'EOF'

Interpretation:
- Any ServiceAccount listed above has Secrets attached; many of these may be
kubernetes.io/service-account-token Secrets.
- Cross-check the secret names here against the legacy token list in section (1).
- ServiceAccounts with attached token Secrets are candidates for migration to
projected TokenRequest-based tokens with audience and expiry.

EOF

echo "=== Review guidance ==="
cat <<'EOF'
A configuration is potentially problematic when:
- A ServiceAccount has one or more kubernetes.io/service-account-token Secrets, AND
- Workloads using that ServiceAccount are not using projected serviceAccountToken volumes
(i.e., they are absent from section (2)).

Use this report to:
- Identify which ServiceAccounts and namespaces still depend on legacy token Secrets.
- Prioritize those for migration to projected tokens with explicit audiences and expiries.
Note: This script only surfaces current state; migration to projected tokens must be
planned and applied manually per application and ServiceAccount.

EOF