Minimize Access To The Service Account Token Creation
More Info:
Access to create the token sub-resource of ServiceAccounts can be used to obtain persistent unauthorized access to the cluster. It should be limited to trusted administrators only.
Risk Level
High
Address
Security
Compliance Standards
- CIS AKS
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
On any machine with kubectl access, list all Roles/ClusterRoles that can create service account tokens (directly or via wildcards):
kubectl get clusterroles -o json \| jq -r '.items[]| select(.rules[]?| select((.apiGroups[]? == "") and(.resources[]? | test("^serviceaccounts(/token)?$")) and(.verbs[]? | test("^(create|\\*)$"))))| .metadata.name' | sort -ukubectl get roles --all-namespaces -o json \| jq -r '.items[]| select(.rules[]?| select((.apiGroups[]? == "") and(.resources[]? | test("^serviceaccounts(/token)?$")) and(.verbs[]? | test("^(create|\\*)$"))))| [.metadata.namespace, .metadata.name] | @tsv' | sort -
For each identified Role/ClusterRole, review the exact rules to confirm whether they include
serviceaccounts/tokenor overly broad patterns:# ClusterRole examplekubectl get clusterrole <CLUSTERROLE_NAME> -o yaml# Role examplekubectl get role <ROLE_NAME> -n <NAMESPACE> -o yamlAssess whether
createonserviceaccounts/token,serviceaccounts, or*is strictly required for that role’s purpose. -
Identify who is actually getting this permission by listing RoleBindings/ClusterRoleBindings that reference each Role/ClusterRole:
kubectl get rolebindings --all-namespaces -o json \| jq -r --arg ROLE "<ROLE_NAME>" '.items[]| select(.roleRef.kind=="Role" and .roleRef.name==$ROLE)| [.metadata.namespace, .metadata.name, .subjects[]?.kind, .subjects[]?.name] | @tsv' | sortkubectl get clusterrolebindings -o json \| jq -r --arg CR "<CLUSTERROLE_NAME>" '.items[]| select(.roleRef.kind=="ClusterRole" and .roleRef.name==$CR)| [.metadata.name, .subjects[]?.kind, .subjects[]?.namespace, .subjects[]?.name] | @tsv' | sortDecide whether each subject (user, group, or service account) is a trusted administrator that should retain this ability.
-
For non-administrative or unnecessary subjects, remove or adjust bindings so they no longer inherit token creation:
# Remove an entire binding that only exists to grant this rightkubectl delete rolebinding <BINDING_NAME> -n <NAMESPACE>kubectl delete clusterrolebinding <BINDING_NAME># Or edit to point to a safer Role/ClusterRole without token creationkubectl edit rolebinding <BINDING_NAME> -n <NAMESPACE>kubectl edit clusterrolebinding <BINDING_NAME>When editing, ensure the referenced Role/ClusterRole does not include
serviceaccounts/tokenor wildcard rules granting it. -
Where a Role/ClusterRole has broader permissions than needed (e.g.,
resources: ["serviceaccounts", "serviceaccounts/token", "*"]orverbs: ["create", "*"]), refine it:kubectl edit clusterrole <CLUSTERROLE_NAME>kubectl edit role <ROLE_NAME> -n <NAMESPACE>Remove
serviceaccounts/tokenand any unnecessary wildcards, preserving only the minimal set of verbs and resources truly required. -
Verify that only intended administrative roles can still create service account tokens:
# Re-run evidence collectionkubectl get clusterroles -o json | jq -r '...same jq as in step 1...'kubectl get roles --all-namespaces -o json | jq -r '...same jq as in step 1...'Confirm that any remaining roles with
createonserviceaccounts/token(or equivalent wildcard) are bound only to trusted administrator identities.
Using kubectl
# 1) List all ClusterRoles that can create serviceaccount tokens
# Run on: any machine with kubectl access
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.apiGroups // [""]) | index("")
)
| select(
(.resources // []) | index("serviceaccounts/token")
)
| select(
(.verbs // []) | index("create")
)
)
| .metadata.name
' | sort -u
Problem indication: Any returned ClusterRole is capable of creating service account tokens. Every such role must be reviewed to ensure it is intended only for trusted administrators.
# 2) Inspect the detailed rules of each suspicious ClusterRole
# Replace <clusterrole-name> with one from the previous command
kubectl get clusterrole <clusterrole-name> -o yaml
What to look for as problematic:
resources:includesserviceaccounts/token(orserviceaccountswith*that covers sub-resources)verbs:includescreateor*- Role name/description does not clearly indicate “admin” or privileged use
- Rules are overly broad, e.g.:
resources: ["*"]or["serviceaccounts", "serviceaccounts/token"]verbs: ["*"]or includes unrelated high-privilege verbs (impersonate,bind,escalate)
# 3) Find which ClusterRoleBindings/RoleBindings grant these ClusterRoles to subjects
# Run once per suspicious ClusterRole
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="<clusterrole-name>")
| .metadata.name
' | sort -u
# 4) Inspect each binding to see who actually gets this privilege
kubectl get clusterrolebinding <binding-name> -o yaml
Problem indication:
subjects:includes:- Broad groups like
system:authenticated,system:serviceaccounts,system:serviceaccounts:<namespace>, orsystem:masters-equivalents where that group is not strictly controlled - Generic or many service accounts, instead of a single, well-known admin account
- External identities (OIDC, AAD, etc.) mapped from large user groups
- Broad groups like
# 5) Check namespace-scoped Roles that may also allow token creation
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.apiGroups // [""]) | index("")
)
| select(
(.resources // []) | index("serviceaccounts/token")
)
| select(
(.verbs // []) | index("create")
)
)
| "\(.metadata.namespace):\(.metadata.name)"
' | sort -u
# 6) Inspect each suspicious Role and its bindings
kubectl get role -n <namespace> <role-name> -o yaml
kubectl get rolebindings -n <namespace> -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name=="<role-name>")
| .metadata.name
' | sort -u
kubectl get rolebinding -n <namespace> <binding-name> -o yaml
Problem indication (namespace Roles/RoleBindings):
- Any non-admin workload service account given
createonserviceaccounts/token - Roles bound to many service accounts in an application namespace
- Roles intended for application use (e.g.,
app-*-role) that includeserviceaccounts/tokenwithcreate
# 7) Verification after changes (re-run discovery)
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.apiGroups // [""]) | index("")
)
| select(
(.resources // []) | index("serviceaccounts/token")
)
| select(
(.verbs // []) | index("create")
)
)
| .metadata.name
' | sort -u
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.apiGroups // [""]) | index("")
)
| select(
(.resources // []) | index("serviceaccounts/token")
)
| select(
(.verbs // []) | index("create")
)
)
| "\(.metadata.namespace):\(.metadata.name)"
' | sort -u
Healthy state: Only tightly controlled, clearly administrative roles (and their bindings) appear in this output. Any role used by regular workloads or broad groups should not have create on serviceaccounts/token.
Automation
#!/usr/bin/env bash
# Run on: any machine with kubectl access and appropriate permissions
# Purpose: Report Roles/ClusterRoles that can create the `token` sub-resource on ServiceAccounts
set -euo pipefail
echo "=== ClusterRoles with access to create serviceaccount 'token' sub-resource ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
rules: (
.rules // []
| map(
select(
((.apiGroups // []) | index("")). // core API group
and
((.resources // []) | index("serviceaccounts/token")) and
((.verbs // []) | index("create"))
)
)
)
}
| select(.rules | length > 0)
| "- ClusterRole: \(.name)\n Rules:\n" +
( .rules[]
| " apiGroups: \((.apiGroups // []) | join(\",\"))\n" +
" resources: \((.resources // []) | join(\",\"))\n" +
" verbs: \((.verbs // []) | join(\",\"))\n"
)
'
echo
echo "=== Namespaced Roles with access to create serviceaccount 'token' sub-resource ==="
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
namespace: .metadata.namespace,
rules: (
.rules // []
| map(
select(
((.apiGroups // []) | index("")). // core API group
and
((.resources // []) | index("serviceaccounts/token")) and
((.verbs // []) | index("create"))
)
)
)
}
| select(.rules | length > 0)
| "- Role: \(.name)\n Namespace: \(.namespace)\n Rules:\n" +
( .rules[]
| " apiGroups: \((.apiGroups // []) | join(\",\"))\n" +
" resources: \((.resources // []) | join(\",\"))\n" +
" verbs: \((.verbs // []) | join(\",\"))\n"
)
'
echo
echo "=== Bindings that grant these permissions (by subject) ==="
# Helper: list all ClusterRoleBindings and RoleBindings in a compact form
echo "--- ClusterRoleBindings ---"
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| "- ClusterRoleBinding: \(.metadata.name)\n" +
" ClusterRole: \(.roleRef.kind)/\(.roleRef.name)\n" +
" Subjects:\n" +
( (.subjects // [])
| map(" kind=\(.kind) name=\(.name) namespace=\(.namespace // \"-\")")
| join("\n")
)
'
echo
echo "--- RoleBindings ---"
kubectl get rolebindings --all-namespaces -o json \
| jq -r '
.items[]
| "- RoleBinding: \(.metadata.name)\n" +
" Namespace: \(.metadata.namespace)\n" +
" Role: \(.roleRef.kind)/\(.roleRef.name)\n" +
" Subjects:\n" +
( (.subjects // [])
| map(" kind=\(.kind) name=\(.name) namespace=\(.namespace // \"-\")")
| join("\n")
)
'
cat <<'EOF'
INTERPRETING THE OUTPUT
-----------------------
1) Any ClusterRole or Role listed in the first two sections has the ability to create the
'token' sub-resource on ServiceAccounts. These are sensitive permissions.
Potential problems include:
- Generic or broad names suggesting non-admin use, e.g.:
- "edit", "view", "developer", "ci", "automation", "default", "system:*"
- Roles intended for applications, service accounts, or non-privileged users.
2) Use the bindings sections to see *who* actually receives these permissions:
- For each sensitive Role/ClusterRole identified above, find corresponding
ClusterRoleBindings/RoleBindings where roleRef.name matches that role name.
- Problematic situations include:
- Bindings granting these permissions to:
- service accounts used by applications
- groups representing many users (e.g., "developers", "all-users")
- unauthenticated/authenticated built-in groups
- generic or shared service accounts
3) Remediation is manual by design:
- For any non-admin subject that does not truly need this capability, update or
remove the binding, or refactor the Role/ClusterRole to remove
"serviceaccounts/token" with "create".
EOF