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. Identify ServiceAccounts using legacy Secret tokens

    • On any machine with kubectl access:
      kubectl get sa --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}' | sort
      For each ServiceAccount, list referenced Secrets:
      kubectl get sa -A -o json | jq -r '.items[] | select(.secrets != null) | "\(.metadata.namespace) \(.metadata.name) -> \(.secrets[].name)"'
      Note ServiceAccounts that have .secrets populated, especially outside kube-system and default bootstrap usage.
  2. Identify long‑lived ServiceAccount token Secrets

    • On any machine with kubectl access, inspect suspected Secrets:
      kubectl get secret -A -o json | jq -r '
      .items[] |
      select(.type == "kubernetes.io/service-account-token") |
      "\(.metadata.namespace) \(.metadata.name) SA=\(.metadata.annotations["kubernetes.io/service-account.name"]) created=\(.metadata.creationTimestamp)"
      ' | sort
    • Flag tokens that are:
      • Used by applications (non‑system namespaces), and
      • Old (e.g., created weeks/months ago), indicating long‑lived credentials.
  3. Review workloads and external systems that consume these Secrets

    • On any machine with kubectl access, find Pods mounting or env‑referencing these Secrets:
      kubectl get pods -A -o json | jq -r '
      .items[] as $pod |
      ($pod.spec.volumes[]? | select(.secret != null) | "VOL \($pod.metadata.namespace) \($pod.metadata.name) -> \(.secret.secretName)")
      , ($pod.spec.containers[]? as $c |
      ($c.env[]? | select(.valueFrom.secretKeyRef != null) |
      "ENV \($pod.metadata.namespace) \($pod.metadata.name) \($c.name) -> \(.valueFrom.secretKeyRef.name)"))
      ' | sort | uniq
    • Separately review CI/CD pipelines, external services, or scripts that may read these Secrets via kubectl or the API. Determine which integrations can be updated to use projected TokenRequest tokens instead of static Secrets.
  4. Plan and configure use of projected ServiceAccount tokens

    • For each workload that currently mounts a ServiceAccount token Secret and can be changed, update its Pod spec (Deployment/StatefulSet/Job, etc.) to use a projected ServiceAccount token volume with audience and expiry, for example:
      volumes:
      - name: sa-token
      projected:
      sources:
      - serviceAccountToken:
      audience: "your-service-audience"
      expirationSeconds: 3600
      path: "token"
    • Apply the updated manifest from any machine with kubectl access:
      kubectl apply -f <updated-manifest>.yaml
    • Ensure the consuming application or external system is updated to read the token from the projected volume path and validate the audience and expiry.
  5. Decommission unnecessary long‑lived ServiceAccount token Secrets

    • After verifying that workloads/external systems function correctly with projected tokens, remove their dependency on the old Secrets:
      • Update ServiceAccount manifests to omit explicit secrets: entries unless strictly required.
        kubectl edit sa <sa-name> -n <namespace>
        Remove any secrets: list entries that correspond to long‑lived token Secrets.
      • Optionally delete unused legacy token Secrets (only after confirming nothing consumes them):
        kubectl delete secret <secret-name> -n <namespace>
  6. Verify reduced reliance on long‑lived ServiceAccount token Secrets

    • On any machine with kubectl access, re‑list ServiceAccounts and Secrets:
      kubectl get sa -A -o json | jq -r '[
      .items[] |
      select(.secrets != null) |
      "\(.metadata.namespace) \(.metadata.name) -> \(.secrets[].name)"
      ] | sort[]'
      kubectl get secret -A -o json | jq -r '
      .items[] |
      select(.type == "kubernetes.io/service-account-token") |
      "\(.metadata.namespace) \(.metadata.name) SA=\(.metadata.annotations["kubernetes.io/service-account.name"]) created=\(.metadata.creationTimestamp)"
      ' | sort
    • Confirm that:
      • Only strictly necessary ServiceAccounts still reference token Secrets.
      • Most application workloads have moved to using projected ServiceAccount tokens with bounded audience and expiry.
Using kubectl
# 1) List all ServiceAccounts and see if any are auto-mounting tokens
# Run on: any machine with kubectl access
kubectl get sa --all-namespaces -o wide

Problem indication:

  • secrets column populated for many ServiceAccounts (older clusters) suggests token Secrets may exist.
  • Widespread use of default ServiceAccounts (no custom SAs) often means many pods share the same long‑lived token.

# 2) Inspect one ServiceAccount in detail (replace NAMESPACE and NAME)
kubectl get sa -n NAMESPACE NAME -o yaml

Problem indication:

  • secrets: list contains items named like NAME-token-xxxxx.
  • automountServiceAccountToken: true (or missing and cluster default is true) means all pods using this SA will receive a token volume by default.
  • No explicit design about which workloads should receive a token.

# 3) Find Secrets that are ServiceAccount tokens (legacy style)
kubectl get secrets --all-namespaces -o jsonpath='{range .items[?(@.type=="kubernetes.io/service-account-token")]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}'

Problem indication:

  • Large numbers of kubernetes.io/service-account-token Secrets indicate long‑lived tokens are broadly available.
  • If these Secrets are mounted into pods (see next step), they are likely being used instead of short‑lived projected tokens.

# 4) Check pods for mounted SA-token Secrets (legacy volume mount style)
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{range .spec.volumes[*]}{" volume: "}{.name}{" type: "}{.projected?"projected":.secret?"secret":"other"}{"\n"}{end}{"---\n"}{end}'

Problem indication:

  • Volumes of type: secret with a secretName matching a *-token-xxxxx Secret show pods mounting long‑lived token Secrets.
  • Very few or no type: projected volumes configured for tokens suggests the cluster is not using projected ServiceAccount tokens.

(For a focused check on a single pod:)

kubectl get pod -n NAMESPACE PODNAME -o yaml

Look under:

  • .spec.volumes for secret: entries with names like *-token-xxxxx.
  • .spec.serviceAccountName and .spec.automountServiceAccountToken.

# 5) Check whether pods are using automounted default tokens vs. explicit projected tokens
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" SA:"}{.spec.serviceAccountName}{" autoToken:"}{.spec.automountServiceAccountToken}{"\n"}{end}'

Problem indication:

  • Many pods with automountServiceAccountToken:true (or null while the SA/namespace/cluster default is true) but without any explicit projected token volume configuration indicate reliance on legacy auto‑mounted tokens.

# 6) Identify ServiceAccounts that are widely used by pods (to prioritize review)
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.spec.serviceAccountName}{"\n"}{end}' | sort | uniq -c | sort -nr

Problem indication:

  • ServiceAccounts with a high pod count are high‑value targets; if they rely on long‑lived token Secrets, the risk is elevated.

# 7) (Optional) Inspect a projected token volume configuration for comparison
# This is to understand what "good" can look like; replace NAMESPACE/POD
kubectl get pod -n NAMESPACE PODNAME -o yaml

Positive indication (for reference):

  • Under .spec.volumes, a projected: volume with a serviceAccountToken: source that specifies:
    • audience: ...
    • expirationSeconds: ...
  • This shows use of bounded, short‑lived tokens rather than legacy Secrets.

Human review guidance (what to decide from this data):

  • If you see many kubernetes.io/service-account-token Secrets mounted into pods as secret volumes, you likely rely on long‑lived tokens and should plan to:
    • Disable automatic token Secret creation (cluster/SA level) going forward.
    • Stop mounting legacy token Secrets into workloads and instead configure projected serviceAccountToken volumes with appropriate audiences and short expiries.
  • If most workloads rely only on default auto‑mounted tokens and no projected token volumes are defined, review which workloads truly need a ServiceAccount token and which can have automountServiceAccountToken: false.
Automation
#!/usr/bin/env bash
# Report ServiceAccounts that use long-lived Secret-based tokens
# versus those configured for projected/TokenRequest-style tokens.
#
# Run on: any machine with kubectl access and current context set.

set -euo pipefail

echo "Collecting ServiceAccount and Secret token usage across the cluster..."
echo

# 1) List all ServiceAccounts and whether they are configured to automount any token
echo "=== ServiceAccounts and automountServiceAccountToken settings ==="
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
sa_automount: ( .automountServiceAccountToken // "unset" ),
pod_default_automount: (
# check namespace-level default for pods (if set via namespace annotation)
.metadata.namespace as $ns
| "" # placeholder, see note below
),
secrets: ( .secrets // [] | map(.name) )
}
| "\(.ns)\t\(.name)\t\(.sa_automount)\t\(.secrets | join(","))"
' \
| awk 'BEGIN {print "NAMESPACE\tSERVICEACCOUNT\tSA_AUTOMOUNT_TOKEN\tBOUND_SECRETS"}1'
echo
echo "NOTE:"
echo "- SA_AUTOMOUNT_TOKEN:"
echo " true = pod(s) using this SA will, by default, mount a token volume"
echo " false = pod(s) using this SA will NOT auto-mount a token volume"
echo " unset = falls back to pod spec or cluster default behavior"
echo "- BOUND_SECRETS lists Secret objects referenced on the ServiceAccount."
echo " These are typically long-lived ServiceAccount token Secrets when of type:"
echo " kubernetes.io/service-account-token"
echo

# 2) List all Secrets that are ServiceAccount tokens and how many SAs reference them
echo "=== Secrets of type kubernetes.io/service-account-token ==="
kubectl get secrets --all-namespaces -o json \
| jq -r '
.items[]
| select(.type == "kubernetes.io/service-account-token")
| {
ns: .metadata.namespace,
name: .metadata.name,
sa_name: .metadata.annotations["kubernetes.io/service-account.name"],
sa_uid: .metadata.annotations["kubernetes.io/service-account.uid"]
}
| "\(.ns)\t\(.name)\t\(.sa_name)\t\(.sa_uid)"
' \
| awk 'BEGIN {print "NAMESPACE\tSECRET\tBOUND_SERVICEACCOUNT\tSERVICEACCOUNT_UID"}1'
echo
echo "INTERPRETATION:"
echo "- Each row is a long-lived Secret-style token bound to a ServiceAccount."
echo "- Large numbers of such Secrets for actively used ServiceAccounts"
echo " suggest reliance on long-lived tokens instead of short-lived projected tokens."
echo

# 3) Identify Pods that are mounting Secret-based SA tokens as volumes
echo "=== Pods that have a Secret volume of type ServiceAccount token ==="
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| . as $pod
| (
.spec.volumes // []
| map(
select(.secret != null)
| {
pod_ns: $pod.metadata.namespace,
pod_name: $pod.metadata.name,
sa_name: ($pod.spec.serviceAccountName // "default"),
volume_name: .name,
secret_name: .secret.secretName
}
)
)
| .[]
| "\(.pod_ns)\t\(.pod_name)\t\(.sa_name)\t\(.volume_name)\t\(.secret_name)"
' 2>/dev/null \
| awk 'BEGIN {print "NAMESPACE\tPOD\tSERVICEACCOUNT\tVOLUME_NAME\tSECRET_NAME"}1' \
|| echo "No pods with Secret volumes found (or jq error)."
echo
echo "INTERPRETATION:"
echo "- Pods listed here explicitly mount Secrets as volumes."
echo "- If SECRET_NAME refers to a ServiceAccount token Secret"
echo " (see list above), then that pod is using a long-lived token Secret."
echo

# 4) OPTIONAL: Spot ServiceAccounts that appear to rely on Secrets AND allow automount
echo "=== ServiceAccounts that both have token Secrets and allow automount ==="
echo "(These are higher-likelihood candidates for review/migration to projected tokens.)"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| select(
# SA is not explicitly disabling automount
(.automountServiceAccountToken != false)
)
| select(
# Has at least one bound secret
(.secrets // [] | length) > 0
)
| {
ns: .metadata.namespace,
name: .metadata.name,
sa_automount: ( .automountServiceAccountToken // "unset" ),
secrets: ( .secrets // [] | map(.name) )
}
| "\(.ns)\t\(.name)\t\(.sa_automount)\t\(.secrets | join(","))"
' \
| awk 'BEGIN {print "NAMESPACE\tSERVICEACCOUNT\tSA_AUTOMOUNT_TOKEN\tBOUND_SECRETS"}1'
echo
echo "INTERPRETATION (high-level):"
echo "- Rows above are priority review targets."
echo "- Problematic patterns to focus on:"
echo " * ServiceAccounts with SA_AUTOMOUNT_TOKEN=true (or unset) AND many token Secrets."
echo " * Workloads (Pods/Deployments/etc.) that mount these Secrets as volumes."
echo
echo "WHAT INDICATES A PROBLEM:"
echo "- Heavy use of Secrets of type kubernetes.io/service-account-token,"
echo " especially for application ServiceAccounts (non-system namespaces)."
echo "- ServiceAccounts used by long-running workloads that:"
echo " * Have SA_AUTOMOUNT_TOKEN=true or unset; AND"
echo " * Have bound ServiceAccount token Secrets; AND/OR"
echo " * Are referenced by Pods mounting those Secrets explicitly."
echo
echo "NEXT STEPS (MANUAL):"
echo "- For flagged ServiceAccounts/workloads, plan to migrate to projected"
echo " ServiceAccount tokens (TokenRequest API), with explicit audience and expiry."
echo "- There is no one-shot automated fix; review each ServiceAccount/workload"
echo " to understand how the token is consumed before changing it."