#!/usr/bin/env bash
set -euo pipefail
# This script must run on any machine with kubectl configured for the target cluster.
# It requires:
# - access to cluster-wide RBAC (cluster-admin or equivalent)
# - kubectl in PATH
echo "=== Detecting RBAC permissions to create serviceaccount token subresources ==="
echo
# Helper: pretty header
header() {
printf '\n===== %s =====\n' "$1"
}
# 1) List ALL ClusterRoles and Roles that can create serviceaccount tokens
header "1) Roles/ClusterRoles that can create serviceaccounts/token"
# We look for:
# - apiGroups: [""]
# - resources: ["serviceaccounts/token"] (preferred new-style)
# - OR resources: ["serviceaccounts"] with resourceNames including "token"
# - verbs including "create"
#
# This prints a line per matching rule: KIND;NAMESPACE;NAME;VERBS;RESOURCES;RESOURCENAMES
kubectl get clusterroles -o json \
| jq -r '
.items[]
| . as $cr
| .rules[]
| select(
(
# explicit subresource
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts/token") != null and
(.verbs // []) | index("create") != null
) or (
# serviceaccounts with specific resourceNames potentially including token
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts") != null and
(.verbs // []) | index("create") != null and
(.resourceNames // []) != null and
((.resourceNames[] | ascii_downcase) | contains("token"))
)
)
| "ClusterRole;;\($cr.metadata.name);\(.verbs|join(","));\(.resources|join(","));\((.resourceNames // [])|join(","))"
' \
| sort || true
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| . as $r
| .rules[]
| select(
(
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts/token") != null and
(.verbs // []) | index("create") != null
) or (
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts") != null and
(.verbs // []) | index("create") != null and
(.resourceNames // []) != null and
((.resourceNames[] | ascii_downcase) | contains("token"))
)
)
| "Role;\($r.metadata.namespace);\($r.metadata.name);\(.verbs|join(","));\(.resources|join(","));\((.resourceNames // [])|join(","))"
' \
| sort || true
echo
echo "Columns: KIND;NAMESPACE;ROLE_NAME;VERBS;RESOURCES;RESOURCENAMES"
echo "Any non-empty output above represents RBAC roles that can create service account tokens."
echo
# 2) For each such role, show which subjects actually receive that permission via bindings
header "2) Bindings (who actually gets those permissions)"
# Build a temp list of all roles with token-create permission
TMP_ROLES="$(mktemp)"
TMP_BINDS="$(mktemp)"
trap 'rm -f "$TMP_ROLES" "$TMP_BINDS"' EXIT
{
kubectl get clusterroles -o json \
| jq -r '
.items[]
| . as $cr
| select(
any(.rules[]?;
(
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts/token") != null and
(.verbs // []) | index("create") != null
) or (
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts") != null and
(.verbs // []) | index("create") != null and
(.resourceNames // []) != null and
((.resourceNames[] | ascii_downcase) | contains("token"))
)
)
)
| "ClusterRole;;\($cr.metadata.name)"
'
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| . as $r
| select(
any(.rules[]?;
(
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts/token") != null and
(.verbs // []) | index("create") != null
) or (
(.apiGroups // []) | index("") != null and
(.resources // []) | index("serviceaccounts") != null and
(.verbs // []) | index("create") != null and
(.resourceNames // []) != null and
((.resourceNames[] | ascii_downcase) | contains("token"))
)
)
)
| "Role;\($r.metadata.namespace);\($r.metadata.name)"
'
} | sort > "$TMP_ROLES"
# Collect all RoleBindings and ClusterRoleBindings
kubectl get rolebindings --all-namespaces -o json > "$TMP_BINDS.role"
kubectl get clusterrolebindings -o json > "$TMP_BINDS.clusterrole"
echo "KIND;BINDING_NAMESPACE;BINDING_NAME;ROLE_KIND;ROLE_NAMESPACE;ROLE_NAME;SUBJECT_KIND;SUBJECT_NAMESPACE;SUBJECT_NAME"
# ClusterRoleBindings
jq -r --argfile roles "$TMP_ROLES" '
def has_role($kind; $ns; $name):
($roles[] | select(.[0]==$kind and .[1]==$ns and .[2]==$name)) | length > 0;
.items[]
| . as $b
| .roleRef as $r
| select(
has_role($r.kind; ""; $r.name)
)
| (.subjects // [{kind:"User",name:"<no-subjects>"}])[]
| [
"ClusterRoleBinding",
"", # binding namespace
$b.metadata.name,
$r.kind,
"", # role namespace
$r.name,
.kind,
(.namespace // ""),
.name
] | @csv
' "$TMP_BINDS.clusterrole" | sed 's/^"\|"$//g;s/","/;/g'
# RoleBindings
jq -r --argfile roles "$TMP_ROLES" '
def has_role($kind; $ns; $name):
($roles[] | select(.[0]==$kind and .[1]==$ns and .[2]==$name)) | length > 0;
.items[]
| . as $b
| .roleRef as $r
| select(
has_role($r.kind; $b.metadata.namespace; $r.name)
)
| (.subjects // [{kind:"User",name:"<no-subjects>"}])[]
| [
"RoleBinding",
$b.metadata.namespace,
$b.metadata.name,
$r.kind,
$b.metadata.namespace,
$r.name,
.kind,
(.namespace // ""),
.name
] | @csv
' "$TMP_BINDS.role" | sed 's/^"\|"$//g;s/","/;/g'
echo
cat <<'EOF'
How to interpret the binding output:
- Each line is:
KIND;BINDING_NAMESPACE;BINDING_NAME;ROLE_KIND;ROLE_NAMESPACE;ROLE_NAME;SUBJECT_KIND;SUBJECT_NAMESPACE;SUBJECT_NAME
- Every line represents a subject that can create service account tokens.
What indicates a potential problem:
1) Any subject that is not a tightly controlled administrator identity, for example:
- SUBJECT_KIND=User or Group representing broad or external users.
- SUBJECT_KIND=ServiceAccount used by application workloads.
- Wildcard or broad groups (e.g., system:authenticated, system:serviceaccounts).
2) Any ClusterRoleBinding that grants this power cluster-wide to non-admin subjects.
3) Roles/ClusterRoles where:
- VERBS contains "*" or includes "create" alongside very broad permissions.
- RESOURCES includes "*" or many unrelated resources.
Next steps (manual review required):
- For any suspect line, decide whether that subject truly needs to mint service account tokens.
- If not required, update or delete the corresponding Role/ClusterRole or Binding to remove create on serviceaccounts/token.
- Re-run this script after changes to confirm only trusted administrator identities remain.
EOF