Skip to main content

Pods That Do Not Use The API Should Disable Token Automount

More Info:

Verifies automountServiceAccountToken is false for pods that do not call the Kubernetes API. A mounted token is a ready-made credential for an attacker who lands in the pod.

Risk Level

Medium

Address

Security

Compliance Standards

  • Cloudanix Best Practice

Triage and Remediation

Remediation

Manual Steps
  1. Identify noncompliant pods and their owners (run on any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
    | select(.spec.automountServiceAccountToken != false)
    | "\($m.namespace) \($m.name) \($own.kind // "Pod") \($own.name // $m.name)"
    ][]'

    Save the list; for each line note: NAMESPACE, POD_NAME, OWNER_KIND, OWNER_NAME.

  2. For each workload, decide if it really needs a service account token (run on any machine with kubectl access):

    • Inspect pod spec and image/command:
      kubectl -n NAMESPACE get pod POD_NAME -o yaml
    • Look for:
      • In-cluster client libraries (e.g., uses KUBERNETES_SERVICE_HOST, kubeconfig, client-go, @kubernetes/client-node).
      • Environment variables, volume mounts, or sidecars that clearly talk to the Kubernetes API.
    • If in doubt, consult the application owner; do not disable token automount until you’re confident it does not call the API.
  3. For standalone Pods that do not need the API, patch the Pod spec (run on any machine with kubectl access):

    kubectl -n NAMESPACE patch pod POD_NAME \
    --type merge \
    -p '{"spec":{"automountServiceAccountToken":false}}'

    Note: If the Pod is controlled by a higher-level object (Deployment, DaemonSet, etc.), this change will be lost when the controller recreates the Pod; in that case, change the controller instead (next step).

  4. For controller-managed workloads that do not need the API, patch the controller spec (run on any machine with kubectl access, pick the right kind per workload):

    Deployment example:

    kubectl -n NAMESPACE patch deployment OWNER_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

    DaemonSet example:

    kubectl -n NAMESPACE patch daemonset OWNER_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

    StatefulSet example:

    kubectl -n NAMESPACE patch statefulset OWNER_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

    Job example:

    kubectl -n NAMESPACE patch job OWNER_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

    CronJob example:

    kubectl -n NAMESPACE patch cronjob OWNER_NAME \
    --type merge \
    -p '{"spec":{"jobTemplate":{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}}}'
  5. (Optional) If multiple pods in a namespace share a ServiceAccount and none of their workloads call the API, you may set it once at the ServiceAccount level instead (run on any machine with kubectl access, and only after confirming no user of the ServiceAccount needs the API):

    kubectl -n NAMESPACE patch serviceaccount SERVICEACCOUNT_NAME \
    --type merge \
    -p '{"automountServiceAccountToken":false}'

    Then ensure any workload that does need the token either uses a different ServiceAccount or explicitly sets automountServiceAccountToken: true in its pod template.

  6. Verify remediation (run on any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | (.spec.automountServiceAccountToken == false) as $ok
    | "ns=\($m.namespace) name=\($m.name) automountServiceAccountToken=\(.spec.automountServiceAccountToken // "unset") is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | map(select(.|contains("is_compliant=false"))) | length) == 0
    then "is_compliant=true"
    else $rows[]
    end'
Using kubectl

On any machine with kubectl access:

  1. Identify the pod and owning controller
kubectl get pods --all-namespaces \
-o wide

For each non‑system pod reported as noncompliant, determine its controller (Deployment, StatefulSet, etc.):

kubectl get pod <POD_NAME> -n <NAMESPACE> -o jsonpath='{.metadata.ownerReferences[0].kind}{" "}{.metadata.ownerReferences[0].name}{"\n"}'

Do not edit bare Pods that are managed by a controller; changes will be overwritten. Always patch the controller.

  1. Patch the controller template to disable token automount

Examples (pick the kind that matches the ownerReferences.kind you saw):

Deployment

kubectl patch deployment <DEPLOYMENT_NAME> -n <NAMESPACE> \
--type='merge' \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

StatefulSet

kubectl patch statefulset <STATEFULSET_NAME> -n <NAMESPACE> \
--type='merge' \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

DaemonSet

kubectl patch daemonset <DAEMONSET_NAME> -n <NAMESPACE> \
--type='merge' \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

If the workload legitimately calls the Kubernetes API, review and skip patching instead of forcing this setting.

  1. Optional: set it on the ServiceAccount instead of each pod

If multiple workloads share a ServiceAccount and none need API access:

kubectl patch serviceaccount <SA_NAME> -n <NAMESPACE> \
--type='merge' \
-p '{"automountServiceAccountToken":false}'
  1. Verification

After controllers have rolled out updated pods, verify:

kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.automountServiceAccountToken == false) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " serviceAccount=\(.spec.serviceAccountName // "default")"
+ " automountServiceAccountToken=\(.spec.automountServiceAccountToken // "unset")"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# Remediate CBP C2.2:
# For pods that do NOT call the Kubernetes API, set automountServiceAccountToken: false
# at the pod level (or via their ServiceAccount).
#
# NOTE: This script CANNOT safely decide which workloads legitimately call the
# Kubernetes API. It:
# - Lists all noncompliant pods (excluding kube-system, kube-public, kube-node-lease)
# - For each, shows the owning controller and current setting
# - Generates example kubectl patches you can apply after review
#
# Run on: any machine with kubectl and jq configured for the target cluster.
# Safe to re-run.

set -euo pipefail

echo "==> Checking for required tools (kubectl, jq)..."
if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq not found in PATH." >&2
exit 1
fi

echo "==> Discovering noncompliant pods (excluding kube-system, kube-public, kube-node-lease)..."
NONCOMPLIANT_JSON="$(kubectl get pods --all-namespaces -o json \
| jq '
.items
| map(
select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| select(.spec.automountServiceAccountToken != false)
)
')"

COUNT="$(printf '%s\n' "${NONCOMPLIANT_JSON}" | jq 'length')"

if [ "${COUNT}" -eq 0 ]; then
echo "==> No noncompliant pods found. Cluster currently compliant for CBP C2.2."
exit 0
fi

echo "==> Found ${COUNT} noncompliant pods."
echo
echo "================================ REVIEW REQUIRED ================================"
echo "The benchmark explicitly says: workloads that legitimately call the API"
echo "must be reviewed rather than blindly remediated."
echo
echo "For each listed pod, determine whether the workload calls the Kubernetes API."
echo "If it does NOT, you can apply the suggested 'kubectl patch' to set"
echo "automountServiceAccountToken=false at the controller or pod level."
echo "==============================================================================="

echo
echo "==> Noncompliant pods and suggested remediation commands (for manual review):"
echo

printf '%s\n' "${NONCOMPLIANT_JSON}" | jq -r '
.[] as $pod
| ($pod.metadata.namespace) as $ns
| ($pod.metadata.name) as $name
| ($pod.spec.serviceAccountName // "default") as $sa
| ($pod.spec.automountServiceAccountToken // "unset") as $am
| ([ ($pod.metadata.ownerReferences // [])[] | select(.controller) ] | first) as $own
| "----------------------------------------------------------------------",
"Pod: " + $ns + "/" + $name,
"SA: " + $sa,
"Current automountServiceAccountToken: " + ($am|tostring),
(if $own == null then
"Owner: (none - naked Pod)"
else
"Owner: " + $own.kind + " " + $ns + "/" + $own.name
end),
"",
"1) Decide if this workload calls the Kubernetes API (code/config review, logs, etc.).",
" - If it DOES call the API: leave token automount enabled (no change).",
" - If it does NOT call the API: apply one of the following, depending on owner:",
"",
(if $own == null then
"# Naked Pod: patch the Pod spec directly (non-persistent; will be lost if Pod is re-created):\n" +
"kubectl patch pod " + $name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"automountServiceAccountToken\":false}}'\n"
else
(if $own.kind == "Deployment" then
"# Deployment owner: patch the Deployment template so all Pods inherit the setting:\n" +
"kubectl patch deploy " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
elif $own.kind == "StatefulSet" then
"# StatefulSet owner: patch the StatefulSet template:\n" +
"kubectl patch statefulset " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
elif $own.kind == "DaemonSet" then
"# DaemonSet owner: patch the DaemonSet template:\n" +
"kubectl patch daemonset " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
elif $own.kind == "ReplicaSet" then
"# ReplicaSet owner (likely managed by a Deployment). Prefer patching the owning Deployment.\n" +
"# If this ReplicaSet is the top-level controller, patch it directly:\n" +
"kubectl patch rs " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
elif $own.kind == "Job" then
"# Job owner: patch the Job template (for future pods):\n" +
"kubectl patch job " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
elif $own.kind == "CronJob" then
"# CronJob owner: patch the CronJob jobTemplate:\n" +
"kubectl patch cronjob " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"jobTemplate\":{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}}}'\n"
else
"# Owner kind " + $own.kind + " is not explicitly handled; patch its pod template accordingly.\n" +
"# Example (adjust resource kind/name as needed):\n" +
"kubectl patch " + ($own.kind | ascii_downcase) + " " + $own.name + " -n " + $ns +
" --type=merge -p '{\"spec\":{\"template\":{\"spec\":{\"automountServiceAccountToken\":false}}}}'\n"
end)
end)
'

echo "==> Review the commands above, apply only to workloads that do NOT call the Kubernetes API."
echo

echo "==> Verification (post-remediation):"
echo "After applying patches to the appropriate workloads, re-run this command:"
echo
echo "kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as \$n | [\"kube-system\",\"kube-public\",\"kube-node-lease\"] | index(\$n) | not)
| .metadata as \$m
| (.spec.automountServiceAccountToken == false) as \$ok
| \"kind=Pod ns=\(\$m.namespace) name=\(\$m.name) is_compliant=\(if \$ok then \"true\" else \"false\" end)\"
] as \$rows
| if (\$rows | length) == 0 then \"is_compliant=true\" else \$rows[] end'"
echo
echo "Any remaining lines with is_compliant=false represent pods still mounting a token."