Skip to main content

Minimize Access To Webhook Configuration Objects

More Info:

Access to validating or mutating webhook configurations can be used to intercept or alter admission decisions. Restrict this access.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. List all ClusterRoles that grant webhook access
    Run on any machine with kubectl access:

    kubectl get clusterroles -o json \
    | jq -r '
    .items[]
    | select(
    .rules[]
    | select(
    (.apiGroups[]? == "admissionregistration.k8s.io")
    and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
    )
    )
    | .metadata.name
    ' | sort -u

    Save the output list of ClusterRole names for review.

  2. Inspect each identified ClusterRole’s permissions
    For each ClusterRole name from step 1:

    CLUSTERROLE_NAME="<clusterrole-name>"
    kubectl get clusterrole "${CLUSTERROLE_NAME}" -o yaml

    Review .rules and note which verbs (get, list, watch, create, update, patch, delete) are allowed on validatingwebhookconfigurations or mutatingwebhookconfigurations.

  3. Determine which subjects receive these ClusterRoles
    For each ClusterRole name from step 1, find ClusterRoleBindings:

    CLUSTERROLE_NAME="<clusterrole-name>"
    kubectl get clusterrolebindings -o json \
    | jq -r --arg cr "${CLUSTERROLE_NAME}" '
    .items[]
    | select(.roleRef.kind == "ClusterRole" and .roleRef.name == $cr)
    | .metadata.name
    '

    For each returned ClusterRoleBinding:

    CRB_NAME="<clusterrolebinding-name>"
    kubectl get clusterrolebinding "${CRB_NAME}" -o yaml

    Review .subjects (users, groups, service accounts) and confirm which ones truly need webhook configuration access.

  4. Decide least-privilege adjustments
    For each ClusterRole and its bindings:

    • Confirm whether any subject actually needs to read or modify webhook configurations (e.g., admission controller operators).
    • If only read is required, plan to remove create, update, patch, delete.
    • If only specific webhook type is required, plan to remove access to the other (validating vs mutating).
    • If a subject does not need any webhook access, plan to:
      • Rebind it to a different, less-privileged ClusterRole, or
      • Remove the ClusterRoleBinding if not needed.
  5. Apply targeted changes to ClusterRoles/ClusterRoleBindings
    For each ClusterRole that needs reduction:

    kubectl edit clusterrole "<clusterrole-name>"

    In the editor, under rules for admissionregistration.k8s.io, remove unnecessary verbs and/or resources, following your decisions from step 4, then save.
    For each ClusterRoleBinding that should no longer grant webhook access:

    kubectl edit clusterrolebinding "<clusterrolebinding-name>"

    Adjust .roleRef to a less-privileged ClusterRole or remove unneeded subjects; if the binding is not needed at all, delete it:

    kubectl delete clusterrolebinding "<clusterrolebinding-name>"
  6. Re-verify effective access after changes
    Re-run the identification command from step 1:

    kubectl get clusterroles -o json \
    | jq -r '
    .items[]
    | select(
    .rules[]
    | select(
    (.apiGroups[]? == "admissionregistration.k8s.io")
    and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
    )
    )
    | .metadata.name
    ' | sort -u

    For any remaining ClusterRoles, confirm via kubectl get clusterrole <name> -o yaml that only the minimal, justified permissions and bindings remain.

Using kubectl
# 1) List all ClusterRoles that can access validating or mutating webhook configurations
# Run on: any machine with kubectl access

kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
[
.rules[]
| select(
(.apiGroups[]? == "admissionregistration.k8s.io")
and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
)
] | length > 0
)
| .metadata.name
' | sort | uniq

Problem indication: Any non-system or broad-privilege role (e.g., custom app roles) listed here may be over-privileged and must be reviewed.


# 2) Show full details of ClusterRoles identified above
# Replace <CLUSTERROLE_NAME> with each name from step 1

kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml

Problem indication: In the rules: section, look for:

  • apiGroups: ["admissionregistration.k8s.io"]
  • resources: ["validatingwebhookconfigurations"] and/or ["mutatingwebhookconfigurations"]
  • verbs including create, update, patch, delete, or *

Roles that are:

  • bound to many subjects,
  • used by application service accounts,
  • or grant write/delete (create, update, patch, delete, *) are high risk and should be questioned.

# 3) Find which subjects (users/groups/serviceaccounts) are bound to those ClusterRoles
# Run for each ClusterRole of interest

kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<CLUSTERROLE_NAME>")
| .metadata.name as $crb
| .subjects[]
| "\($crb)\t\(.kind)\t\(.namespace // "-")\t\(.name)"
' | column -t

Problem indication: Any binding that grants these roles to:

  • generic groups (e.g., system:authenticated, system:unauthenticated),
  • broad human user groups (e.g., devs, admins not specifically for cluster ops),
  • application service accounts in non-system namespaces

suggests webhook configuration access is too widely granted.


# 4) Quick summary: show only roles with WRITE access to webhooks
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
[
.rules[]
| select(
(.apiGroups[]? == "admissionregistration.k8s.io")
and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
and (.verbs[]? | IN("create","update","patch","delete","*"))
)
] | length > 0
)
| .metadata.name
' | sort | uniq

Problem indication: Any role in this list must have a clear, justified operational need; otherwise it is a candidate for permissions reduction.


# 5) Inspect direct access on the webhook objects themselves (cluster-wide)
# Read access is also sensitive (can expose configuration patterns).

kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations

Problem indication: Large numbers of webhook configs, or unfamiliar names owned by third parties, warrant closer scrutiny of who manages them using steps 1–4.

Automation
#!/usr/bin/env bash
set -euo pipefail

# This script inspects which subjects can access validating/mutating webhook configurations.
# Run on any machine with kubectl access and cluster-wide RBAC permissions.

echo "=== 1) ClusterRoles with access to validatingwebhookconfigurations or mutatingwebhookconfigurations ==="
kubectl get clusterrole -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
rules: (
.rules // []
| map(
select(
(.apiGroups[]? == "admissionregistration.k8s.io")
and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
)
)
)
}
| select((.rules | length) > 0)
| . as $r
| "ClusterRole: \($r.name)\nRules:",
(
$r.rules[]
| " apiGroups: \(.apiGroups // [])",
" resources: \(.resources // [])",
" verbs: \(.verbs // [])",
""
)' \
| sed 's/"//g' || echo "Failed to query ClusterRoles"

echo
echo "=== 2) ClusterRoleBindings mapping those ClusterRoles to subjects ==="
problematic_crs=$(kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
select(
(.apiGroups[]? == "admissionregistration.k8s.io")
and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
)
)
| length > 0
)
| .metadata.name
')

if [ -z "$problematic_crs" ]; then
echo "No ClusterRoles found with direct access to webhook configuration objects."
else
for cr in $problematic_crs; do
echo "ClusterRole: ${cr}"
kubectl get clusterrolebinding -o json \
| jq -r --arg CR "$cr" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $CR)
| " ClusterRoleBinding: \(.metadata.name)",
" Subjects:",
(
.subjects // []
| .[]
| " kind=\(.kind) name=\(.name) namespace=\(.namespace // "-")"
),
""
'
echo
done
fi

echo "=== 3) (Optional) Namespaced Roles with access (less common) ==="
kubectl get role -A -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
rules: (
.rules // []
| map(
select(
(.apiGroups[]? == "admissionregistration.k8s.io")
and (.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
)
)
)
}
| select((.rules | length) > 0)
| . as $r
| "Role: \($r.ns)/\($r.name)\nRules:",
(
$r.rules[]
| " apiGroups: \(.apiGroups // [])",
" resources: \(.resources // [])",
" verbs: \(.verbs // [])",
""
)' \
| sed 's/"//g' || echo "Failed to query Roles"

cat <<'EOF'

How to interpret this output:

- Any ClusterRole or Role listed above that grants non-read-only verbs on:
resources: ["validatingwebhookconfigurations","mutatingwebhookconfigurations"]
apiGroups: ["admissionregistration.k8s.io"]
is potentially problematic.

- Pay particular attention to:
- verbs including: ["create","update","patch","delete","deletecollection","*"]
- broad roles like "cluster-admin" or custom admin/devops roles
- ClusterRoleBindings/RoleBindings that attach these roles to:
* system:authenticated or system:serviceaccounts
* groups or service accounts not strictly required to manage admission webhooks

- Safer patterns:
- No roles with these resources at all, except a very small number of tightly controlled
admin/SRE roles.
- If access is needed, limit verbs to ["get","list","watch"] where possible, and bind
only to specific trusted subjects.

Use this report to review and decide which roles/bindings should be tightened or removed.
EOF