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
Remediation
Manual Steps
-
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' -
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.
-
Inspect the exact permissions granted on serviceaccounts/token
- For each role/clusterrole from step 1:
# ClusterRoleCLUSTERROLE_NAME="<name>"kubectl get clusterrole "$CLUSTERROLE_NAME" -o yaml# Namespaced RoleNAMESPACE="<namespace>"ROLE_NAME="<name>"kubectl get role -n "$NAMESPACE" "$ROLE_NAME" -o yaml- Confirm whether
resources: ["serviceaccounts/token"]is necessary and whetherverbscan be reduced or removed.
-
Tighten or remove token-creation permissions
- If token creation is not needed, edit and remove the
serviceaccounts/tokenrule:
# ClusterRoleCLUSTERROLE_NAME="<name>"kubectl edit clusterrole "$CLUSTERROLE_NAME"# RoleNAMESPACE="<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 removecreatefromverbsif you must retain other verbs.
- If token creation is not needed, edit and remove the
-
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.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:name: sa-token-minimalrules:- apiGroups: [""]resources: ["serviceaccounts/token"]verbs: ["create"]EOFkubectl apply -f sa-token-minimal-clusterrole.yaml# Example binding to a single service accountkubectl 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.
-
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
verbscontainscreatedirectly 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:authenticatedorsystem: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.ServiceAccountthat do not clearly need to mint tokens (generic app SAs).Useraccounts 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
ClusterRoleorRolelistingresources: ["serviceaccounts/token"](or including it in the list) andverbsincluding"create". - Corresponding
ClusterRoleBindingorRoleBindingsections that attach these roles to:subjectswithkind=UserorGrouprepresenting broad human populations (e.g.system:authenticated, large SSO groups), orsubjectswithkind=ServiceAccountthat do not strictly require minting tokens for other workloads.
- A
-
These cases should be manually reviewed; where possible, remove
createonserviceaccounts/tokenfrom those roles or refactor the access to narrower, justified subjects.