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

    • 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
    | (.spec.automountServiceAccountToken == false) as $ok
    | select($ok | not)
    | "ns=\($m.namespace) name=\($m.name)"
    + (if $own == null then "" else " owner=\($own.kind)/\($own.name)" end)
    ][]'
  2. For a standalone Pod: capture the manifest

    • Run on: any machine with kubectl access
      Replace NAMESPACE and POD_NAME with values from step 1:
    kubectl get pod POD_NAME -n NAMESPACE -o yaml > /tmp/pod-POD_NAME.yaml
  3. Edit the pod manifest to disable token automount

    • Run on: any machine with kubectl access
    • Open the file and add or update the field under spec:
    sed -i 's/^spec:$/spec:\n automountServiceAccountToken: false/' /tmp/pod-POD_NAME.yaml
    • If automountServiceAccountToken already exists under spec:, edit it so it reads:
    spec:
    automountServiceAccountToken: false
  4. Recreate the pod with the updated manifest

    • Run on: any machine with kubectl access
    kubectl delete pod POD_NAME -n NAMESPACE --wait=true
    kubectl apply -f /tmp/pod-POD_NAME.yaml
  5. For controller-managed workloads (e.g., Deployment, DaemonSet, Job): patch the controller instead of the pod

    • Run on: any machine with kubectl access
    • Example for a Deployment; adjust KIND, NAME, and NAMESPACE based on the owner= from step 1:
    kubectl -n NAMESPACE patch deployment OWNER_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'
    • The controller will automatically recreate pods with the new setting.
  6. Verify compliance

    • 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
    | select($ok | not)
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Using kubectl

On any machine with kubectl access to the OKE cluster:

  1. Identify a non‑compliant pod and its owner (from the audit output), for example:

    • Namespace: app-namespace
    • Pod name: web-7c8b4f9d9b-xj9lt
    • Owner: Deployment/app-namespace/web
  2. Export the owning workload manifest, edit it locally, and re‑apply (pod spec fields like automountServiceAccountToken must be set on the controller, not on the individual pod):

kubectl -n app-namespace get deploy web -o yaml > web-deploy.yaml

Edit web-deploy.yaml and in spec.template.spec add (or change) this field:

apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: app-namespace
spec:
template:
spec:
automountServiceAccountToken: false
# ... other fields remain unchanged

Apply the updated manifest:

kubectl apply -f web-deploy.yaml

Kubernetes will recreate the pods managed by this Deployment with automountServiceAccountToken: false. Repeat this pattern for other controller types (StatefulSet, DaemonSet, Job, CronJob) by adjusting the kind in the kubectl get command and editing the same spec.template.spec section.

  1. If the pod is created directly (no controller ownerReference), edit the pod spec and re‑create it:
kubectl -n app-namespace get pod web-standalone -o yaml > web-standalone-pod.yaml

Edit web-standalone-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: web-standalone
namespace: app-namespace
spec:
automountServiceAccountToken: false
# ... other fields remain unchanged

Delete and recreate the pod:

kubectl -n app-namespace delete pod web-standalone
kubectl apply -f web-standalone-pod.yaml
  1. Optional: set this at the ServiceAccount level instead of each pod, if all pods using it do not need API access:
kubectl -n app-namespace get serviceaccount web-sa -o yaml > web-sa.yaml

Edit web-sa.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
name: web-sa
namespace: app-namespace
automountServiceAccountToken: false

Apply:

kubectl apply -f web-sa.yaml
  1. Verification (rerun the audit command):
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
#
# Disable automountServiceAccountToken for pods in OKE that do NOT need
# the Kubernetes API, by defaulting to a secure ServiceAccount setting.
#
# RUN ON: any machine with kubectl access and jq installed.
#
# NOTES:
# - This script does NOT try to guess which pods use the API.
# - It only operates on namespaces you explicitly approve.
# - It configures the *ServiceAccount* used by pods:
# automountServiceAccountToken: false
# so new pods using that SA will not auto-mount tokens.
# - Existing running pods will keep their current mounts until restarted.

set -euo pipefail

# ---------- CONFIGURATION ----------

# Comma-separated list of namespaces you have reviewed and decided that
# workloads there do NOT need to call the Kubernetes API.
# Example: SAFE_NAMESPACES="dev,test,frontend"
SAFE_NAMESPACES="${SAFE_NAMESPACES:-}"

if [[ -z "${SAFE_NAMESPACES}" ]]; then
echo "ERROR: SAFE_NAMESPACES is empty."
echo "Set SAFE_NAMESPACES to a comma-separated list of non-system namespaces"
echo "whose pods you have reviewed and determined do NOT need the Kubernetes API."
echo "Example:"
echo " SAFE_NAMESPACES='dev,test,frontend' $0"
exit 1
fi

# Convert SAFE_NAMESPACES to an array
IFS=',' read -r -a NS_LIST <<< "${SAFE_NAMESPACES}"

# Namespaces that must always be excluded
EXCLUDED_NS_REGEX='^(kube-system|kube-public|kube-node-lease)$'

echo "Using SAFE_NAMESPACES: ${SAFE_NAMESPACES}"
echo

# ---------- FUNCTIONS ----------

ensure_namespace_allowed() {
local ns="$1"
if [[ "${ns}" =~ ${EXCLUDED_NS_REGEX} ]]; then
return 1
fi
return 0
}

# Patch or create ServiceAccounts with automountServiceAccountToken: false
harden_service_accounts_in_namespace() {
local ns="$1"

if ! ensure_namespace_allowed "${ns}"; then
echo "Skipping system namespace: ${ns}"
return
fi

echo "Processing namespace: ${ns}"

# Get all ServiceAccounts in the namespace (including 'default')
local sa_list
if ! sa_list=$(kubectl get sa -n "${ns}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null); then
echo " WARNING: Failed to list ServiceAccounts in namespace ${ns}, skipping."
return
fi

if [[ -z "${sa_list}" ]]; then
echo " No ServiceAccounts found in namespace ${ns}."
return
fi

while IFS= read -r sa; do
[[ -z "${sa}" ]] && continue

# Check current value (may be null/unset)
local current
current=$(kubectl get sa "${sa}" -n "${ns}" -o jsonpath='{.automountServiceAccountToken}' 2>/dev/null || echo "")

if [[ "${current}" == "false" ]]; then
echo " SA ${ns}/${sa}: automountServiceAccountToken already false"
continue
fi

echo " Patching SA ${ns}/${sa}: setting automountServiceAccountToken=false"
kubectl patch sa "${sa}" -n "${ns}" \
--type merge \
-p '{"automountServiceAccountToken": false}' >/dev/null
done <<< "${sa_list}"
}

# Verify by re-running a focused version of the audit logic
verify_namespace() {
local ns="$1"

echo
echo "Verification for namespace: ${ns}"

kubectl get pods -n "${ns}" -o json | jq -r '
[ .items[]
| .metadata as $m
| (.spec.automountServiceAccountToken == false) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) " +
"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 "no_pods_in_namespace" else $rows[] end'
}

# ---------- MAIN ----------

for ns in "${NS_LIST[@]}"; do
ns_trimmed="$(echo "${ns}" | xargs)"
[[ -z "${ns_trimmed}" ]] && continue

if ! kubectl get ns "${ns_trimmed}" >/dev/null 2>&1; then
echo "Namespace ${ns_trimmed} not found; skipping."
continue
fi

harden_service_accounts_in_namespace "${ns_trimmed}"
verify_namespace "${ns_trimmed}"
done

echo
echo "Completed. For cluster-wide verification, run:"
echo " kubectl get pods --all-namespaces -o json | jq -r '...<full audit expression>...'"