Skip to main content

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

Manual Steps
  1. 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 -u

    kubectl 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
  2. For each identified Role/ClusterRole, review its intent and current subjects (who gets it):

    # Example for a ClusterRole
    kubectl get clusterrole <NAME> -o yaml

    # See who is bound to it
    kubectl get clusterrolebindings -o yaml \
    | yq 'select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<NAME>")'

    # Example for a namespaced Role
    kubectl get role -n <NAMESPACE> <NAME> -o yaml
    kubectl 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.

  3. 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 subjects
    kubectl edit clusterrolebinding <BINDING_NAME>

    # Or a RoleBinding in a namespace
    kubectl edit rolebinding -n <NAMESPACE> <BINDING_NAME>

    In the editor, delete subjects that should not have CSR approval. Save to apply.

  4. 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 rule
    kubectl edit clusterrole <NAME>
    # or
    kubectl edit role -n <NAMESPACE> <NAME>

    In the rules, either delete the certificatesigningrequests/approval entry or reduce verbs so update (and * if present) is not granted unless absolutely required.

  5. 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:authenticated or large SSO groups. Adjust bindings accordingly:

    kubectl edit clusterrolebinding <BINDING_NAME>
    # Replace broad/unknown groups with specific, documented admin identities.
  6. Verify that only the intended admin identities retain CSR approval rights:

    # Re-run discovery
    kubectl 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):

  • resources includes certificatesigningrequests/approval or certificatesigningrequests or *
  • verbs includes update or *
  • 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:

  • kind is Group with a very broad group (e.g., system:authenticated, system:serviceaccounts, or an org-wide group not limited to cluster admins).
  • kind is ServiceAccount used by general workloads, CI/CD, or applications, rather than a tightly-controlled admin SA.
  • kind is User representing 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., default in any namespace).
    • Application-specific service accounts that are not trusted cluster administrators.