Minimize Access To The Approval Sub-Resource Of
More Info:
Access to approve CertificateSigningRequests can be abused to issue high-privilege certificates and create cluster-admin level users. This access 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 approve CSRs:
kubectl get clusterroles -o json \| jq -r '.items[]| select(.rules[]?| select(.apiGroups[]? == "certificates.k8s.io")| select(.resources[]? == "certificatesigningrequests/approval")| select(.verbs[]? | IN("update","*")))| .metadata.name' | sort -ukubectl get roles --all-namespaces -o json \| jq -r '.items[]| select(.rules[]?| select(.apiGroups[]? == "certificates.k8s.io")| select(.resources[]? == "certificatesigningrequests/approval")| select(.verbs[]? | IN("update","*")))| "\(.metadata.namespace)/\(.metadata.name)"' | sort -u -
For each identified Role/ClusterRole, review its intent and current subjects (who gets it):
# Example for a ClusterRolekubectl get clusterrole <NAME> -o yaml# See who is bound to itkubectl get clusterrolebindings -o yaml \| yq 'select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<NAME>")'# Example for a namespaced Rolekubectl get role -n <NAMESPACE> <NAME> -o yamlkubectl get rolebindings -n <NAMESPACE> -o yaml \| yq 'select(.roleRef.kind == "Role" and .roleRef.name == "<NAME>")'Decide whether each subject (user/group/serviceaccount) is a trusted admin who truly needs CSR approval.
-
For access that is not justified, remove or narrow the binding instead of deleting the Role/ClusterRole outright:
# Edit a specific ClusterRoleBinding to remove non-admin subjectskubectl edit clusterrolebinding <BINDING_NAME># Or a RoleBinding in a namespacekubectl edit rolebinding -n <NAMESPACE> <BINDING_NAME>In the editor, delete subjects that should not have CSR approval. Save to apply.
-
If a Role/ClusterRole is used exclusively to provide CSR approval to non-admins and is no longer needed, remove or tighten its rule:
# Edit to remove or restrict the CSR approval rulekubectl edit clusterrole <NAME># orkubectl edit role -n <NAMESPACE> <NAME>In the
rules, either delete thecertificatesigningrequests/approvalentry or reduceverbssoupdate(and*if present) is not granted unless absolutely required. -
For cases where limited, justified CSR approvers are required (e.g., a small security team), ensure they are mapped to a dedicated admin group or service account instead of broad subjects like
system:authenticatedor large SSO groups. Adjust bindings accordingly:kubectl edit clusterrolebinding <BINDING_NAME># Replace broad/unknown groups with specific, documented admin identities. -
Verify that only the intended admin identities retain CSR approval rights:
# Re-run discoverykubectl get clusterroles -o json | jq -r '...same jq as step 1...'kubectl get roles --all-namespaces -o json | jq -r '...same jq as step 1...'For each remaining Role/ClusterRole listed, confirm its bindings reference only trusted admin users/groups/serviceaccounts as per your access policy.
Using kubectl
# 1. List all ClusterRoles that can approve CSRs
# Run on: any machine with kubectl access
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| (.verbs[]? | IN("update","*"))
and (.resources[]? | IN("certificatesigningrequests/approval","certificatesigningrequests","*"))
)
| .metadata.name
' | sort -u
Problem indication: Any ClusterRole in this list is able (directly or via *) to approve CSRs. Each listed role must be reviewed to decide if that power is justified and restricted to trusted admins.
# 2. Show full definitions of those ClusterRoles for detailed review
# Replace <CLUSTERROLE_NAME> with each name from the previous command
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
What to look for (problem indicators):
resourcesincludescertificatesigningrequests/approvalorcertificatesigningrequestsor*verbsincludesupdateor*- The role is bound to broad subjects (e.g.,
system:authenticated, wide groups, or service accounts used by general workloads), not just a very small set of trusted admins.
# 3. Identify what subjects are bound to each CSR-approving ClusterRole
# Run once per ClusterRole name from step 1
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="<CLUSTERROLE_NAME>")
| .metadata.name as $rb
| .subjects[]
| "\($rb) \t \(.kind) \t \(.namespace // "-") \t \(.name)"
' | sort
Problem indication: Any binding where:
kindisGroupwith a very broad group (e.g.,system:authenticated,system:serviceaccounts, or an org-wide group not limited to cluster admins).kindisServiceAccountused by general workloads, CI/CD, or applications, rather than a tightly-controlled admin SA.kindisUserrepresenting many operators instead of a very small set of trusted admins.
# 4. Check Namespaced Roles (if using aggregated or custom approval flows)
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules[]
| (.verbs[]? | IN("update","*"))
and (.resources[]? | IN("certificatesigningrequests/approval","certificatesigningrequests","*"))
)
| "\(.metadata.namespace)\t\(.metadata.name)"
' | sort
Then inspect and trace bindings:
# 4a. Inspect a specific Role
kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml
# 4b. See who has that Role in that namespace
kubectl get rolebindings -n <NAMESPACE> -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")
| .metadata.name as $rb
| .subjects[]
| "\($rb) \t \(.kind) \t \(.namespace // "-") \t \(.name)"
' | sort
Problem indication: Same patterns as for ClusterRoles—any Role with CSR approval power that is bound to non-admin or broadly scoped subjects.
# 5. (Optional) Quick human-readable summary of all CSR-approval rules
kubectl get clusterroles -o json \
| jq -r '
.items[]
| .metadata.name as $name
| [
.rules[]
| select(
(.verbs[]? | IN("update","*"))
and (.resources[]? | IN("certificatesigningrequests/approval","certificatesigningrequests","*"))
)
| " - apiGroups: \(.apiGroups|join(",")) resources: \(.resources|join(",")) verbs: \(.verbs|join(","))"
] as $matches
| select($matches|length>0)
| $name, ($matches[])
'
Use this summary to visually confirm which roles can approve CSRs and decide, case by case, whether that is acceptable or needs to be restricted.
Automation
#!/usr/bin/env bash
# Report Roles/ClusterRoles that can approve CertificateSigningRequests
# Run on: any machine with kubectl access and current kubeconfig context
set -euo pipefail
echo "=== ClusterRoles with CSR approval rights ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select(
(
# Direct CSR approval subresource
($r | index("certificatesigningrequests/approval"))
# Wildcards that include it
or ($r | index("certificatesigningrequests") and ($v | index("*") or $v | index("update") or $v | index("patch")))
or (($r | index("*") or $r | index("*/approval")) and ($v | index("*") or $v | index("update") or $v | index("patch")))
)
)
)
| length > 0
)
| .metadata.name
' \
| sort -u
echo
echo "=== Roles (namespaced) with CSR approval rights ==="
kubectl get roles -A -o json \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select(
(
($r | index("certificatesigningrequests/approval"))
or ($r | index("certificatesigningrequests") and ($v | index("*") or $v | index("update") or $v | index("patch")))
or (($r | index("*") or $r | index("*/approval")) and ($v | index("*") or $v | index("update") or $v | index("patch")))
)
)
)
| length > 0
)
| [.metadata.namespace, .metadata.name]
| @tsv
' \
| sort -u
echo
echo "=== ClusterRoleBindings & RoleBindings that grant CSR approval rights ==="
# Helper: list RBAC objects that bind to roles/clusterroles from stdin
list_bindings_for_roles() {
local role_kind="$1" # ClusterRole or Role
while read -r name ns; do
if [[ -z "${name:-}" ]]; then
continue
fi
if [[ "${role_kind}" == "ClusterRole" ]]; then
kubectl get clusterrolebindings -o json \
| jq -r --arg rn "$name" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $rn)
| .metadata.name
' \
| sed "s/^/${role_kind} ${name} <- ClusterRoleBinding: /"
kubectl get rolebindings -A -o json \
| jq -r --arg rn "$name" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $rn)
| [.metadata.namespace, .metadata.name]
| @tsv
' \
| sed "s/^/${role_kind} ${name} <- RoleBinding: /"
else
# Namespaced Role
kubectl get rolebindings -n "$ns" -o json \
| jq -r --arg rn "$name" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $rn)
| .metadata.name
' \
| sed "s/^/${role_kind} ${ns}/${name} <- RoleBinding: /"
fi
done
}
echo "# Bindings for ClusterRoles with CSR approval:"
clusterroles_with_csr=$(kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select(
(
($r | index("certificatesigningrequests/approval"))
or ($r | index("certificatesigningrequests") and ($v | index("*") or $v | index("update") or $v | index("patch")))
or (($r | index("*") or $r | index("*/approval")) and ($v | index("*") or $v | index("update") or $v | index("patch")))
)
)
)
| length > 0
)
| .metadata.name
' | sort -u)
if [[ -n "${clusterroles_with_csr}" ]]; then
awk '{print $1}' <<< "${clusterroles_with_csr}" | list_bindings_for_roles "ClusterRole"
else
echo "None"
fi
echo
echo "# Bindings for Roles with CSR approval:"
roles_with_csr=$(kubectl get roles -A -o json \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select(
(
($r | index("certificatesigningrequests/approval"))
or ($r | index("certificatesigningrequests") and ($v | index("*") or $v | index("update") or $v | index("patch")))
or (($r | index("*") or $r | index("*/approval")) and ($v | index("*") or $v | index("update") or $v | index("patch")))
)
)
)
| length > 0
)
| [.metadata.namespace, .metadata.name]
| @tsv
' | sort -u)
if [[ -n "${roles_with_csr}" ]]; then
list_bindings_for_roles "Role" <<< "${roles_with_csr}"
else
echo "None"
fi
echo
echo "=== Interpretation guide ==="
echo "- Any ClusterRole or Role listed above has the ability (directly or via wildcards) to approve CSRs."
echo "- Investigate bindings that grant these roles to service accounts, groups, or users that are not trusted administrators."
echo "- Overly broad examples to flag:"
echo " * Roles/ClusterRoles with resources: ['*'] and verbs including '*' or 'update' or 'patch'."
echo " * Any binding of such roles to default service accounts or wide groups (e.g., system:authenticated)."
Problematic output indicators to review and potentially restrict:
- ClusterRoles with names like
edit,admin, or custom application roles that appear in the “ClusterRoles with CSR approval rights” list. - Roles in application namespaces (non-system namespaces) that appear in the “Roles (namespaced) with CSR approval rights” list.
- Any of the listed Roles/ClusterRoles bound (via ClusterRoleBinding or RoleBinding) to:
system:authenticated,system:serviceaccounts, or similar broad groups.- Default service accounts (e.g.,
defaultin any namespace). - Application-specific service accounts that are not trusted cluster administrators.