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
Remediation
Manual Steps
-
Identify ServiceAccounts using legacy Secret tokens
- On any machine with kubectl access:
For each ServiceAccount, list referenced Secrets:kubectl get sa --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}' | sortNote ServiceAccounts that havekubectl get sa -A -o json | jq -r '.items[] | select(.secrets != null) | "\(.metadata.namespace) \(.metadata.name) -> \(.secrets[].name)"'
.secretspopulated, especially outsidekube-systemanddefaultbootstrap usage.
- On any machine with kubectl access:
-
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.
- On any machine with kubectl access, inspect suspected Secrets:
-
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
kubectlor the API. Determine which integrations can be updated to use projected TokenRequest tokens instead of static Secrets.
- On any machine with kubectl access, find Pods mounting or env‑referencing these Secrets:
-
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-tokenprojected:sources:- serviceAccountToken:audience: "your-service-audience"expirationSeconds: 3600path: "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.
- 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:
-
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.Remove anykubectl edit sa <sa-name> -n <namespace>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>
- Update ServiceAccount manifests to omit explicit
- After verifying that workloads/external systems function correctly with projected tokens, remove their dependency on the old Secrets:
-
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.
- On any machine with kubectl access, re‑list ServiceAccounts and Secrets:
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:
secretscolumn 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 likeNAME-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-tokenSecrets 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: secretwith asecretNamematching a*-token-xxxxxSecret show pods mounting long‑lived token Secrets. - Very few or no
type: projectedvolumes 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.volumesforsecret:entries with names like*-token-xxxxx..spec.serviceAccountNameand.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, aprojected:volume with aserviceAccountToken: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-tokenSecrets mounted into pods assecretvolumes, 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
serviceAccountTokenvolumes 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."