Skip to main content

Service Account Tokens Are Only Mounted Where Necessary

More Info:

Automatically mounting service account tokens into pods that do not call the API server expands the attack surface. Disable token mounting on service accounts and pods that do not need it.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS OKE

Triage and Remediation

Remediation

Manual Steps
  1. Identify service accounts and pods that auto-mount tokens

    • Run on any machine with kubectl access:
      kubectl get serviceaccounts --all-namespaces -o json \
      | jq -r '.items[] | [.metadata.namespace, .metadata.name,
      ( .automountServiceAccountToken // "default(true)" )] | @tsv'
      kubectl get pods --all-namespaces -o json \
      | jq -r '.items[] | [.metadata.namespace, .metadata.name,
      ( .spec.automountServiceAccountToken // "inherit" ),
      .spec.serviceAccountName] | @tsv'
  2. Select candidates that do not need API access

    • Focus on namespaces and workloads that are not control‑plane or infrastructure (e.g., non‑kube-system, non‑monitoring/ingress).
    • For each candidate deployment/statefulset/cronjob, inspect its containers for API use:
      kubectl get deploy -A -o yaml > /tmp/all-deployments.yaml
    • Review /tmp/all-deployments.yaml (or per‑namespace exports) for images and arguments that clearly do not interact with the Kubernetes API (simple stateless apps, frontends, batch jobs with no cluster logic).
  3. Confirm with application owners whether API access is required

    • For each candidate workload, provide: namespace, resource name, serviceAccountName, and current automountServiceAccountToken behavior (from step 1).
    • Ask owners if the pod ever: uses in‑cluster config, lists/watches Kubernetes objects, or needs token‑based authentication. If not, mark it safe to disable token mounting.
  4. Disable token auto-mount on service accounts where safe

    • For a specific service account confirmed as not needing API access (example: namespace prod, service account web-sa), run:
      kubectl patch serviceaccount web-sa -n prod \
      -p '{"automountServiceAccountToken": false}'
    • For new service accounts, ensure manifests include:
      apiVersion: v1
      kind: ServiceAccount
      metadata:
      name: web-sa
      namespace: prod
      automountServiceAccountToken: false
  5. Override token auto-mount at pod spec level where needed

    • For workloads using shared service accounts where some pods need tokens and others do not, set automountServiceAccountToken: false on the pod template of the specific workload. Examples:
      kubectl -n prod patch deploy web-frontend \
      --type merge \
      -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'
    • Persist this in the source manifests/Helm charts so it is not reverted by future deployments.
  6. Verify changes cluster-wide

    • Re-run the cluster-wide inspection:
      kubectl get serviceaccounts --all-namespaces -o json \
      | jq -r '.items[] | [.metadata.namespace, .metadata.name,
      ( .automountServiceAccountToken // "default(true)" )] | @tsv'
      kubectl get pods --all-namespaces -o json \
      | jq -r '.items[] | [.metadata.namespace, .metadata.name,
      ( .spec.automountServiceAccountToken // "inherit" ),
      .spec.serviceAccountName] | @tsv'
    • Confirm that pods and service accounts identified as not needing API access now have automountServiceAccountToken set to false either on the service account or on the pod spec.
Using kubectl

Using kubectl

1. List all ServiceAccounts and check their token mounting behavior

Run on: any machine with kubectl access.

kubectl get serviceaccounts --all-namespaces -o yaml > /tmp/all-serviceaccounts.yaml

Review /tmp/all-serviceaccounts.yaml and look for automountServiceAccountToken:

  • If missing on a ServiceAccount, it inherits the namespace/pod default (usually true unless overridden).
  • If explicitly set to true, that ServiceAccount’s pods will mount tokens by default.
  • For ServiceAccounts used by workloads that do not need to call the API server, automountServiceAccountToken: true or unset is a potential problem; those are candidates to set automountServiceAccountToken: false after review.

To focus on ServiceAccounts that explicitly enable token mounting:

kubectl get serviceaccounts --all-namespaces -o json \
| jq -r '.items[]
| select(.automountServiceAccountToken == true)
| [.metadata.namespace, .metadata.name, .automountServiceAccountToken]
| @tsv'

Output columns:

  1. Namespace
  2. ServiceAccount name
  3. true

Each line represents a ServiceAccount that forces token mounting; verify whether its pods truly need API access.

2. Find pods that explicitly control token mounting

Run on: any machine with kubectl access.

kubectl get pods --all-namespaces -o yaml > /tmp/all-pods.yaml

Inspect /tmp/all-pods.yaml and look for automountServiceAccountToken:

  • If automountServiceAccountToken: true at pod spec level, the pod will mount a token even if the ServiceAccount is more restrictive.
  • If automountServiceAccountToken: false, the pod is explicitly protected and will not mount a token.
  • Pods with automountServiceAccountToken: true that do not need to call the API server are potential problems.

To list pods that explicitly enable token mounting:

kubectl get pods --all-namespaces -o json \
| jq -r '.items[]
| select(.spec.automountServiceAccountToken == true)
| [.metadata.namespace, .metadata.name, .spec.serviceAccountName, .spec.automountServiceAccountToken]
| @tsv'

Output columns:

  1. Namespace
  2. Pod name
  3. ServiceAccount name
  4. true

Each line is a pod that forces token mounting; review the workload purpose to decide if that is justified.

3. Identify pods that implicitly mount tokens (likely default behavior)

These pods do not set automountServiceAccountToken but are still likely getting a token via default behavior.

Run on: any machine with kubectl access.

kubectl get pods --all-namespaces -o json \
| jq -r '.items[]
| select(.spec.automountServiceAccountToken == null)
| [.metadata.namespace, .metadata.name, .spec.serviceAccountName]
| @tsv'

Output columns:

  1. Namespace
  2. Pod name
  3. ServiceAccount name

Interpretation:

  • These pods do not override token mounting; whether they receive a token depends on:
    • The ServiceAccount’s automountServiceAccountToken field, or
    • The namespace/global defaults.
  • For workloads that do not need API calls, confirm whether their ServiceAccounts are unnecessarily allowing token mounting.

4. Check if pods actually have token files mounted

This helps confirm effective behavior, independent of spec fields.

Run on: any machine with kubectl access.

for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
for pod in $(kubectl get pods -n "$ns" --no-headers -o custom-columns=':metadata.name'); do
echo "=== $ns/$pod ==="
kubectl exec -n "$ns" "$pod" -- sh -c 'ls -l /var/run/secrets/kubernetes.io/serviceaccount 2>/dev/null || echo "no token directory"'
done
done

Interpretation:

  • If you see files such as token, ca.crt, and namespace under /var/run/secrets/kubernetes.io/serviceaccount, that pod has a service account token mounted.
  • For pods that do not need to interact with the Kubernetes API, the presence of this directory and token file indicates unnecessary exposure.

5. Verification after making manual changes

After you manually update manifests/ServiceAccounts (outside of this section), re-run:

kubectl get serviceaccounts --all-namespaces -o yaml > /tmp/all-serviceaccounts.yaml
kubectl get pods --all-namespaces -o yaml > /tmp/all-pods.yaml

Confirm that:

  • ServiceAccounts and pods that do not need API access either:
    • have automountServiceAccountToken: false, and/or
    • no longer show a token directory when checked with the command in step 4.
Automation
#!/usr/bin/env bash
# Audit service account token mounting across the whole cluster
# Run on: any machine with kubectl access and current context set

set -euo pipefail

echo "=== 1) Cluster-wide ServiceAccount defaults (automountServiceAccountToken) ==="
echo
echo "# Namespaces where the default for ServiceAccounts is NOT explicitly disabled (potentially risky):"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
automount: ( .automountServiceAccountToken // "inherited" )
}
| [ .ns, .name, .automount ]
| @tsv' \
| column -t

cat <<'EOF'

Interpretation:
- If "automount" is "true": this ServiceAccount will mount a token into pods by default (higher risk).
- If "automount" is "false": this ServiceAccount will NOT mount a token by default (more secure).
- If "automount" is "inherited": it follows the namespace or cluster default; review pods using it.

Risk indicators:
- ServiceAccounts used by workloads that do NOT need to call the API server but show "true" or "inherited".
EOF

echo
echo "=== 2) Pods and their effective token-mount setting ==="
echo
echo "# Listing all pods with the effective automount behavior and what it is derived from:"
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| . as $pod
| $pod.spec.serviceAccountName as $sa
| $pod.metadata.namespace as $ns
| $pod.metadata.name as $podname
| (
if $pod.spec.automountServiceAccountToken != null then
{source:"pod", value:$pod.spec.automountServiceAccountToken}
else
(
(input_filename | .) as $f
)
end
)' 2>/dev/null || true

The above jq block is complex to inline correctly across all clusters; instead, use the following more explicit two-step approach:

#!/usr/bin/env bash
set -euo pipefail

echo "=== 2) Pods and their effective token-mount setting (explicit view) ==="
echo
# For each pod, show:
# - namespace, pod name
# - serviceAccountName
# - pod-level automountServiceAccountToken (if set)
# - ServiceAccount-level automountServiceAccountToken (if set)
# Note: effective behavior is:
# pod.automount (if not null) else SA.automount (if not null) else namespace/cluster default (usually true)
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| .metadata.namespace as $ns
| .metadata.name as $podname
| .spec.serviceAccountName as $sa
| .spec.automountServiceAccountToken as $pod_auto
| {
ns: $ns,
pod: $podname,
sa: $sa,
pod_auto: (if $pod_auto==null then "unset" else $pod_auto|tostring end)
}
| @base64' \
| while read -r line; do
obj=$(echo "$line" | base64 -d)
ns=$(echo "$obj" | jq -r '.ns')
pod=$(echo "$obj" | jq -r '.pod')
sa=$(echo "$obj" | jq -r '.sa')
pod_auto=$(echo "$obj" | jq -r '.pod_auto')

# Look up SA-level automount for this pod
sa_auto=$(kubectl get sa "$sa" -n "$ns" -o json 2>/dev/null \
| jq -r '.automountServiceAccountToken // "unset"' || echo "missing-SA")

printf "%s\t%s\t%s\tpod_auto=%s\tsa_auto=%s\n" "$ns" "$pod" "$sa" "$pod_auto" "$sa_auto"
done \
| column -t | sed '1iNAMESPACE POD SERVICEACCOUNT POD_AUTOMOUNT SA_AUTOMOUNT'

cat <<'EOF'

Interpretation:
- POD_AUTOMOUNT=unset and SA_AUTOMOUNT=unset:
- Pod inherits namespace/cluster default (commonly true) -> likely mounts a token.
- POD_AUTOMOUNT=true:
- Pod will mount a token regardless of SA/namespace defaults (higher risk if API not needed).
- POD_AUTOMOUNT=false:
- Pod will NOT mount a token even if SA/namespace defaults are true (more secure).
- SA_AUTOMOUNT=true:
- All pods using this SA (that do not override at pod level) will mount a token.
- SA_AUTOMOUNT=false:
- Pods using this SA will not mount a token unless they explicitly set POD_AUTOMOUNT=true.

Risk indicators (potential problems to review):
- Pods that do NOT need to talk to the Kubernetes API but show:
- POD_AUTOMOUNT=true, OR
- POD_AUTOMOUNT=unset and SA_AUTOMOUNT=true or unset (i.e., likely mounting a token).
- Namespaces or teams where *every* ServiceAccount has automount=true or unset by default.
EOF

To verify the script:

  • Run it from any machine with kubectl and jq installed.
  • Confirm it completes without errors and prints:
    • A table of ServiceAccounts and their automountServiceAccountToken.
    • A table of pods showing pod-level and ServiceAccount-level automount values for review.