Skip to main content

Minimize Access To Service Account Token Creation

More Info:

The token sub-resource of service accounts can mint tokens that impersonate those accounts. Limit who can create service account tokens.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. Identify roles/clusterroles that can create service account tokens

    • Run on: any machine with kubectl access
    kubectl get clusterroles -o json | jq -r '
    .items[] |
    select(.rules[]? |
    .resources[]? == "serviceaccounts/token"
    and (.verbs[]? | IN("create","*"))
    ) |
    .metadata.name
    '
    kubectl get roles -A -o json | jq -r '
    .items[] |
    select(.rules[]? |
    .resources[]? == "serviceaccounts/token"
    and (.verbs[]? | IN("create","*"))
    ) |
    [.metadata.namespace, .metadata.name] | @tsv
    '
  2. Review which subjects are bound to those roles/clusterroles

    • For each ClusterRole name from step 1:
    CLUSTERROLE_NAME="<name from step 1>"
    kubectl get clusterrolebindings -o yaml | yq '
    .items[] |
    select(.roleRef.kind == "ClusterRole" and .roleRef.name == "'"$CLUSTERROLE_NAME"'")
    '
    • For each Role (namespace, name) from step 1:
    NAMESPACE="<namespace>"
    ROLE_NAME="<role name>"
    kubectl get rolebindings -n "$NAMESPACE" -o yaml | yq '
    .items[] |
    select(.roleRef.kind == "Role" and .roleRef.name == "'"$ROLE_NAME"'")
    '
    • Manually decide whether each subject (user/group/serviceaccount) truly needs to mint tokens.
  3. Inspect the exact permissions granted on serviceaccounts/token

    • For each role/clusterrole from step 1:
    # ClusterRole
    CLUSTERROLE_NAME="<name>"
    kubectl get clusterrole "$CLUSTERROLE_NAME" -o yaml

    # Namespaced Role
    NAMESPACE="<namespace>"
    ROLE_NAME="<name>"
    kubectl get role -n "$NAMESPACE" "$ROLE_NAME" -o yaml
    • Confirm whether resources: ["serviceaccounts/token"] is necessary and whether verbs can be reduced or removed.
  4. Tighten or remove token-creation permissions

    • If token creation is not needed, edit and remove the serviceaccounts/token rule:
    # ClusterRole
    CLUSTERROLE_NAME="<name>"
    kubectl edit clusterrole "$CLUSTERROLE_NAME"

    # Role
    NAMESPACE="<namespace>"
    ROLE_NAME="<name>"
    kubectl edit role -n "$NAMESPACE" "$ROLE_NAME"
    • In the editor, delete only the rule(s) that contain resources: ["serviceaccounts/token"] (or that list it among resources), or at least remove create from verbs if you must retain other verbs.
  5. Re-assess bindings; re-scope if needed

    • Where token creation is required but too broadly granted, create a new, minimal Role/ClusterRole and bind it only to the specific service accounts/users that need it:
    cat <<'EOF' > sa-token-minimal-clusterrole.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: sa-token-minimal
    rules:
    - apiGroups: [""]
    resources: ["serviceaccounts/token"]
    verbs: ["create"]
    EOF

    kubectl apply -f sa-token-minimal-clusterrole.yaml

    # Example binding to a single service account
    kubectl create clusterrolebinding sa-token-minimal-binding \
    --clusterrole=sa-token-minimal \
    --serviceaccount=default:example-sa
    • Then remove broader bindings discovered in step 2 that no longer need this access.
  6. Verify that access to serviceaccounts/token is minimized

    • Re-run the discovery from step 1 and confirm only intentionally authorized roles remain:
    kubectl get clusterroles -o json | jq -r '
    .items[] |
    select(.rules[]? |
    .resources[]? == "serviceaccounts/token"
    and (.verbs[]? | IN("create","*"))
    ) |
    .metadata.name
    '
    kubectl get roles -A -o json | jq -r '
    .items[] |
    select(.rules[]? |
    .resources[]? == "serviceaccounts/token"
    and (.verbs[]? | IN("create","*"))
    ) |
    [.metadata.namespace, .metadata.name] | @tsv
    '
    • Optionally verify for a given subject whether it can still create a token:
    kubectl auth can-i create serviceaccounts/token --as=<user-or-sa> -n <namespace>
Using kubectl
# 1) List all Roles and ClusterRoles that mention 'serviceaccounts/token'

# Run on: any machine with kubectl access
kubectl get clusterroles -o yaml | grep -nA5 -B5 "serviceaccounts/token" || echo "No ClusterRoles grant serviceaccounts/token"
kubectl get roles -A -o yaml | grep -nA5 -B5 "serviceaccounts/token" || echo "No Roles grant serviceaccounts/token"

What to look for (problematic cases):

Any rules entry that includes:

resources:
- serviceaccounts/token
verbs:
- create # most critical
- '*' # also implies create
# or any list that includes 'create'

# 2) Show full definitions of Roles/ClusterRoles that reference 'serviceaccounts/token'

# ClusterRoles
kubectl get clusterroles -o jsonpath='{range .items[?(@.rules)]}{.metadata.name}{"\n"}{range .rules[?(@.resources && (index .resources 0)=="serviceaccounts/token" || "serviceaccounts/token" in @.resources)]}{" resources: "}{.resources}{"\n verbs: "}{.verbs}{"\n"}{end}{"\n"}{end}'

# Namespaced Roles
kubectl get roles -A -o jsonpath='{range .items[?(@.rules)]}{.metadata.namespace}{"/"}{.metadata.name}{"\n"}{range .rules[?(@.resources && (index .resources 0)=="serviceaccounts/token" || "serviceaccounts/token" in @.resources)]}{" resources: "}{.resources}{"\n verbs: "}{.verbs}{"\n"}{end}{"\n"}{end}'

What to look for (problematic cases):

  • Any entry where verbs contains create directly or via *.
  • Wide or sensitive scope, for example:
    • ClusterRole used cluster‑wide by many subjects.
    • Roles/ClusterRoles bound to:
      • system:serviceaccounts (all service accounts in a namespace or cluster).
      • system:authenticated or system:authenticated:oauth.
      • Broad user groups (e.g., corporate SSO groups) not specifically needing token minting.

# 3) Find which subjects are bound to those Roles/ClusterRoles

# First, list all RoleBindings/ClusterRoleBindings that reference Roles/ClusterRoles
# mentioning 'serviceaccounts/token'. This is two-step: get names, then describe bindings.

# 3a) Get names of ClusterRoles with serviceaccounts/token
clusterroles_with_token=$(kubectl get clusterroles -o jsonpath='{range .items[?(@.rules)]}{.metadata.name}{" "}{end}' | xargs -n1 -I{} sh -c 'kubectl get clusterrole {} -o yaml | grep -q "serviceaccounts/token" && echo {}')

# 3b) Get names of Roles with serviceaccounts/token (namespaced)
roles_with_token=$(kubectl get roles -A -o jsonpath='{range .items[?(@.rules)]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{end}' | xargs -n1 -I{} sh -c 'ns="${%%/*}"; name="${##*/}"; kubectl get role -n "$ns" "$name" -o yaml | grep -q "serviceaccounts/token" && echo "$ns/$name"' 2>/dev/null)

echo "ClusterRoles with serviceaccounts/token:"
echo "$clusterroles_with_token"
echo
echo "Roles with serviceaccounts/token:"
echo "$roles_with_token"

# 3c) For each such ClusterRole, show its bindings and subjects
for cr in $clusterroles_with_token; do
echo "=== ClusterRole: $cr ==="
kubectl get clusterrolebinding -o jsonpath="{range .items[?(@.role.name=='$cr')]}{.metadata.name}{'\n Subjects: '}{range .subjects}{.kind}{'/'}{.namespace}{'/'}{.name}{', '}{end}{'\n\n'}{end}"
kubectl get rolebinding -A -o jsonpath="{range .items[?(@.role.kind=='ClusterRole' && @.role.name=='$cr')]}{.metadata.namespace}{'/'}{.metadata.name}{'\n Subjects: '}{range .subjects}{.kind}{'/'}{.namespace}{'/'}{.name}{', '}{end}{'\n\n'}{end}"
done

# 3d) For each namespaced Role, show its bindings and subjects
for r in $roles_with_token; do
ns="${r%%/*}"
name="${r##*/}"
echo "=== Role: $ns/$name ==="
kubectl get rolebinding -n "$ns" -o jsonpath="{range .items[?(@.role.kind=='Role' && @.role.name=='$name')]}{.metadata.name}{'\n Subjects: '}{range .subjects}{.kind}{'/'}{.namespace}{'/'}{.name}{', '}{end}{'\n\n'}{end}"
done

What to look for (problematic cases):

  • Bindings where subjects are:
    • Group = system:authenticated, system:serviceaccounts, or large identity provider groups.
    • ServiceAccount that do not clearly need to mint tokens (generic app SAs).
    • User accounts that should not impersonate arbitrary service accounts.

# 4) Focused inspection for 'create' verb on serviceaccounts/token

# ClusterRoles
kubectl get clusterroles -o json | jq -r '
.items[]
| {name: .metadata.name, rules: .rules}
| select(.rules != null)
| select(.rules[]
| select(.resources != null and (.resources | index("serviceaccounts/token")))
| select(.verbs | index("create") or index("*"))
)
| .name
' | while read cr; do
echo "=== ClusterRole needing review: $cr ==="
kubectl get clusterrole "$cr" -o yaml
echo
done

# Roles
kubectl get roles -A -o json | jq -r '
.items[]
| {ns: .metadata.namespace, name: .metadata.name, rules: .rules}
| select(.rules != null)
| select(.rules[]
| select(.resources != null and (.resources | index("serviceaccounts/token")))
| select(.verbs | index("create") or index("*"))
)
| "\(.ns)/\(.name)"
' | while read rn; do
ns="${rn%%/*}"
name="${rn##*/}"
echo "=== Role needing review: $ns/$name ==="
kubectl get role -n "$ns" "$name" -o yaml
echo
done

What to look for (problematic cases):

  • Roles/ClusterRoles from this list that:
    • Are not strictly necessary for components that really need to mint service account tokens.
    • Are bound to broad or sensitive subjects as described above.

# 5) Verification after any manual changes (re‑run core discovery)

# Run on: any machine with kubectl access
kubectl get clusterroles -o yaml | grep -nA5 -B5 "serviceaccounts/token" || echo "No ClusterRoles grant serviceaccounts/token"
kubectl get roles -A -o yaml | grep -nA5 -B5 "serviceaccounts/token" || echo "No Roles grant serviceaccounts/token"

If this output shows no serviceaccounts/token with verbs including create (or *), or only tightly scoped, explicitly justified roles, the risk from this control has been reduced.

Automation
#!/usr/bin/env bash
# Audit serviceaccount token subresource usage in RBAC
# Run on: any machine with kubectl access

set -euo pipefail

echo "=== Searching for RBAC rules that allow 'create' on 'serviceaccounts/token' ==="
echo

# 1) ClusterRoles
echo "--- ClusterRoles with 'create' on serviceaccounts/token ---"
kubectl get clusterroles -o json \
| jq -r '
.items[]
| .metadata.name as $name
| [
.rules[]
| select(.resources != null)
| select((.resources | index("serviceaccounts/token")) != null)
| select((.verbs | index("create")) != null)
] as $rules
| select($rules | length > 0)
| "ClusterRole: \($name)\n" +
(
$rules[]
| " apiGroups: \(.apiGroups // [])\n" +
" resources: \(.resources // [])\n" +
" verbs: \(.verbs // [])\n"
)
'

echo
# 2) Roles (namespaced)
echo "--- Roles with 'create' on serviceaccounts/token ---"
kubectl get roles -A -o json \
| jq -r '
.items[]
| .metadata.name as $name
| .metadata.namespace as $ns
| [
.rules[]
| select(.resources != null)
| select((.resources | index("serviceaccounts/token")) != null)
| select((.verbs | index("create")) != null)
] as $rules
| select($rules | length > 0)
| "Role: \($name) (namespace: \($ns))\n" +
(
$rules[]
| " apiGroups: \(.apiGroups // [])\n" +
" resources: \(.resources // [])\n" +
" verbs: \(.verbs // [])\n"
)
'

echo
# 3) Show which subjects get these permissions (ClusterRoleBindings)
echo "--- ClusterRoleBindings that reference those ClusterRoles ---"
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| .metadata.name as $crb_name
| .roleRef.name as $role_name
| .subjects as $subs
| select($subs != null and ($subs | length > 0))
| "ClusterRoleBinding: \($crb_name)\n roleRef: \($role_name)\n subjects:\n" +
(
$subs[]
| " kind=\(.kind) name=\(.name) namespace=\(.namespace // "-")\n"
)
' \
| awk '
BEGIN { show=0 }
/^ClusterRoleBinding:/ { show=0 }
/roleRef:/ {
role=$2
# Defer filtering; we will check against actual risky ClusterRoles below
print_line=$0
}
{ print }
' >/dev/null # This section is informational; see step 4 for precise mapping

# 4) Precise mapping: cluster-wide subjects that can create serviceaccounts/token
echo "--- Cluster-wide subjects with 'create' on serviceaccounts/token ---"
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
any(.rules[]?;
(.resources // []) | index("serviceaccounts/token")
and (.verbs // []) | index("create")
)
)
| .metadata.name
' \
| sort -u \
| while read -r cr; do
[ -z "$cr" ] && continue
echo "ClusterRole: $cr"
kubectl get clusterrolebindings -o json \
| jq -r --arg cr "$cr" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $cr)
| " ClusterRoleBinding: \(.metadata.name)\n" +
(
(.subjects // [])
| map(" kind=\(.kind) name=\(.name) namespace=\(.namespace // \"-\")")
| join("\n")
)
'
echo
done

# 5) Precise mapping: namespaced subjects that can create serviceaccounts/token
echo "--- Namespaced subjects with 'create' on serviceaccounts/token ---"
kubectl get roles -A -o json \
| jq -r '
.items[]
| select(
any(.rules[]?;
(.resources // []) | index("serviceaccounts/token")
and (.verbs // []) | index("create")
)
)
| "\(.metadata.namespace) \(.metadata.name)"
' \
| sort -u \
| while read -r ns role; do
[ -z "$ns" ] && continue
echo "Role: $role (namespace: $ns)"
kubectl get rolebindings -n "$ns" -o json \
| jq -r --arg role "$role" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $role)
| " RoleBinding: \(.metadata.name)\n" +
(
(.subjects // [])
| map(" kind=\(.kind) name=\(.name) namespace=\(.namespace // \"-\")")
| join("\n")
)
'
echo
done

echo "=== Done. Review output above. ==="

How to interpret the output

  • A potential problem is indicated whenever you see:

    • A ClusterRole or Role listing resources: ["serviceaccounts/token"] (or including it in the list) and verbs including "create".
    • Corresponding ClusterRoleBinding or RoleBinding sections that attach these roles to:
      • subjects with kind=User or Group representing broad human populations (e.g. system:authenticated, large SSO groups), or
      • subjects with kind=ServiceAccount that do not strictly require minting tokens for other workloads.
  • These cases should be manually reviewed; where possible, remove create on serviceaccounts/token from those roles or refactor the access to narrower, justified subjects.