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. On any machine with kubectl access, list the non-compliant pods and choose one to review (replace NAMESPACE and POD_NAME in the next steps accordingly):

    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
    | select($ok | not)
    | "ns=\($m.namespace) name=\($m.name)"
    ][]'
  2. Still on any machine with kubectl access, inspect the chosen pod to determine whether it legitimately calls the Kubernetes API (look for in-cluster client libraries, API server URLs, or service account token usage in args/env/config):

    kubectl -n NAMESPACE get pod POD_NAME -o yaml

    If the workload needs to call the Kubernetes API, document the exception and do not change automountServiceAccountToken for this pod.

  3. If the pod does not need Kubernetes API access and is controlled by a higher-level object (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.), identify that owner:

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

    Then edit the owner resource’s pod template to disable token automount:

    kubectl -n NAMESPACE edit OWNER_KIND OWNER_NAME

    In the opened YAML, under spec.template.spec, add or set:

    automountServiceAccountToken: false

    Save and exit to trigger a rolling update of the pods.

  4. If the pod is not controlled by a higher-level object (no ownerReferences or kind is “Pod”), edit the pod spec directly (note this will not persist across re-creates from external systems):

    kubectl -n NAMESPACE edit pod POD_NAME

    Under spec, add or set:

    automountServiceAccountToken: false
  5. As an alternative (and where appropriate), you may set this at the ServiceAccount level so all pods using it disable token automount by default. On any machine with kubectl access:

    kubectl -n NAMESPACE edit serviceaccount SERVICEACCOUNT_NAME

    Add or set:

    automountServiceAccountToken: false

    Then ensure pods that should not have tokens use this ServiceAccount in their pod templates.

  6. Verify compliance 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
    | "kind=Pod ns=\($m.namespace) name=\($m.name) automountServiceAccountToken=\(if .spec.automountServiceAccountToken == null then "unset" else .spec.automountServiceAccountToken end) is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

    Confirm that pods which do not need API access now show automountServiceAccountToken=false and is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify the noncompliant pod and its owner (from the audit output), for example:

    • Namespace: my-namespace
    • Pod name: my-app-6f7b9d8c7d-abcde
    • Owner: Deployment/my-app
  2. Export the owning workload manifest (example for a Deployment):

kubectl -n my-namespace get deployment my-app -o yaml > my-app-deployment.yaml
  1. Edit the manifest locally (my-app-deployment.yaml) and set automountServiceAccountToken: false in the pod spec. For example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
spec:
template:
spec:
automountServiceAccountToken: false
containers:
- name: my-app
image: myregistry/my-app:1.0.0
  1. Apply the updated manifest:
kubectl apply -f my-app-deployment.yaml
  1. (Optional) If the pod is created directly (no owner), patch it in place:
kubectl -n my-namespace patch pod my-pod \
--type merge \
-p '{"spec":{"automountServiceAccountToken":false}}'
  1. Verification (same style as the audit, on any kubectl machine):
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=\(if .spec.automountServiceAccountToken == null then "unset" else .spec.automountServiceAccountToken end)"
+ " 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
set -euo pipefail

# This script:
# - Lists all Pods that do NOT have automountServiceAccountToken=false set at pod level
# - Skips kube-system, kube-public, kube-node-lease
# - For each such Pod whose owner is a higher-level controller (Deployment, StatefulSet, etc.),
# it PATCHes the owning workload spec.template to set automountServiceAccountToken=false.
# - It does NOT touch bare Pods (no controller ownerReference), because they may be ephemeral or system-created.
#
# Run on: any machine with kubectl access to the AKS cluster.
# Requirements: kubectl, jq

# Safety guard: ensure we can reach the cluster
kubectl version --short >/dev/null

echo "Discovering non-compliant Pods (excluding kube-system, kube-public, kube-node-lease)..."

NON_COMPLIANT_JSON=$(kubectl get pods --all-namespaces -o json | jq -c '
.items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| select(.spec.automountServiceAccountToken != false)
| {
namespace: .metadata.namespace,
pod: .metadata.name,
owner: (
[(.metadata.ownerReferences // [])[] | select(.controller)] | first
)
}
')

if [[ -z "${NON_COMPLIANT_JSON}" ]]; then
echo "No non-compliant Pods found. Nothing to do."
exit 0
fi

echo "Evaluating Pods and their owners..."
echo

# Helper: map Pod owner.kind to the scalable controller resource that owns the Pod template
map_owner_kind_to_resource() {
local kind="$1"
case "${kind}" in
Deployment) echo "deployments" ;;
ReplicaSet) echo "replicasets" ;;
StatefulSet) echo "statefulsets" ;;
DaemonSet) echo "daemonsets" ;;
Job) echo "jobs" ;;
CronJob) echo "cronjobs" ;;
*)
# Unknown or bare Pod owner; return empty to skip
echo ""
;;
esac
}

# Process each Pod
while IFS= read -r item; do
ns=$(jq -r '.namespace' <<< "${item}")
pod=$(jq -r '.pod' <<< "${item}")
owner_present=$(jq -r 'has("owner") and .owner != null' <<< "${item}")

if [[ "${owner_present}" != "true" ]]; then
echo "Skipping bare Pod ${ns}/${pod} (no controller ownerReference). Please review manually if it should disable token automount."
continue
fi

owner_kind=$(jq -r '.owner.kind' <<< "${item}")
owner_name=$(jq -r '.owner.name' <<< "${item}")

resource=$(map_owner_kind_to_resource "${owner_kind}")
if [[ -z "${resource}" ]]; then
echo "Skipping Pod ${ns}/${pod} (owner kind ${owner_kind} not handled by this script). Review manually."
continue
fi

echo "Processing ${owner_kind} ${ns}/${owner_name} (from Pod ${pod})..."

# Patch the controller's Pod template to set automountServiceAccountToken=false
# This is idempotent: repeated patches keep the same value.
patch='{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

kubectl -n "${ns}" patch "${resource}" "${owner_name}" --type=merge -p "${patch}" >/dev/null

echo " Patched ${owner_kind} ${ns}/${owner_name} to set spec.template.spec.automountServiceAccountToken=false"
done <<< "${NON_COMPLIANT_JSON}"

echo
echo "Waiting briefly for controllers to reconcile and new Pods to appear..."
sleep 10

echo
echo "Re-running compliance audit to 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=\(if .spec.automountServiceAccountToken == null then "unset" else .spec.automountServiceAccountToken end)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'