Skip to main content

Minimize Access To Webhook Configuration Objects

More Info:​

Access to validating or mutating webhook configurations can be abused to escalate privileges or disrupt cluster operations. It should be limited to trusted administrators only.

Risk Level​

High

Address​

Security

Compliance Standards​

  • CIS AKS

Triage and Remediation​

Remediation​

Manual Steps
  1. List all Roles/ClusterRoles with webhook configuration permissions
    Run on: any machine with kubectl access

    kubectl get clusterroles -o json | jq -r '
    .items[]
    | select(
    (.rules[]? | .resources[]? | contains("validatingwebhookconfigurations"))
    or
    (.rules[]? | .resources[]? | contains("mutatingwebhookconfigurations"))
    )
    | .metadata.name' | sort -u
    kubectl get roles -A -o json | jq -r '
    .items[]
    | select(
    (.rules[]? | .resources[]? | contains("validatingwebhookconfigurations"))
    or
    (.rules[]? | .resources[]? | contains("mutatingwebhookconfigurations"))
    )
    | "\(.metadata.namespace)/\(.metadata.name)"' | sort -u
  2. Inspect the exact permissions on each identified Role/ClusterRole
    Run on: any machine with kubectl access
    For each ClusterRole name from step 1:

    kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml

    For each namespaced Role from step 1:

    kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml

    Review rules for verbs like create, update, patch, or delete on validatingwebhookconfigurations or mutatingwebhookconfigurations. Flag any Role/ClusterRole not strictly needed for trusted cluster administration.

  3. Identify who is bound to these Roles/ClusterRoles
    Run on: any machine with kubectl access
    For each ClusterRole:

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

    For each Role:

    kubectl get rolebindings -n <NAMESPACE> -o json | jq -r '
    .items[]
    | select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")
    | .metadata.name'

    Then inspect each binding to see which users/groups/serviceaccounts have access:

    kubectl get clusterrolebinding <CRB_NAME> -o yaml
    kubectl get rolebinding -n <NAMESPACE> <RB_NAME> -o yaml
  4. Decide required vs. unnecessary access

    • Classify each subject (user, group, serviceaccount) as:
      • Trusted administrator requiring webhook configuration management, or
      • Application/operator component needing only read access (if any), or
      • Unnecessary recipient of webhook configuration write access.
    • For non-admin components that must manage their own webhooks, prefer narrowly scoped Roles limited to their specific webhooks rather than broad cluster-wide permissions.
  5. Adjust RBAC to remove or narrow write access
    Run on: any machine with kubectl access
    a) To remove an unnecessary binding entirely:

    kubectl delete clusterrolebinding <CRB_NAME>
    kubectl delete rolebinding -n <NAMESPACE> <RB_NAME>

    b) To narrow a Role/ClusterRole (e.g., drop write verbs or webhook resources), edit it:

    kubectl edit clusterrole <CLUSTERROLE_NAME>
    kubectl edit role -n <NAMESPACE> <ROLE_NAME>

    In the editor, remove create, update, patch, delete verbs or remove validatingwebhookconfigurations / mutatingwebhookconfigurations from resources for any non-admin use. Save to apply.

  6. Verify resulting access is limited to trusted administrators
    Run on: any machine with kubectl access
    Re-run the discovery and manually confirm bindings:

    kubectl get clusterroles -o json | jq -r '
    .items[]
    | select(
    (.rules[]? | .resources[]? | contains("validatingwebhookconfigurations"))
    or
    (.rules[]? | .resources[]? | contains("mutatingwebhookconfigurations"))
    )
    | .metadata.name' | sort -u
    kubectl get roles -A -o json | jq -r '
    .items[]
    | select(
    (.rules[]? | .resources[]? | contains("validatingwebhookconfigurations"))
    or
    (.rules[]? | .resources[]? | contains("mutatingwebhookconfigurations"))
    )
    | "\(.metadata.namespace)/\(.metadata.name)"' | sort -u

    For the remaining Roles/ClusterRoles, confirm via their RoleBindings/ClusterRoleBindings that only trusted administrator identities retain write access to webhook configuration objects.

Using kubectl
# 1) List all ClusterRoles that can access validating webhooks
# Run on: any machine with kubectl access
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
(.rules[]?
| select(
(.resources[]? | IN("validatingwebhookconfigurations")) and
(.verbs[]? | IN("create","update","patch","delete","deletecollection","*"))
)
)?
)
| .metadata.name
' | sort -u

What to look for

  • Output is a list of ClusterRole names.
  • Potential problems:
    • Generic or broad roles like cluster-admin (expected but high‑risk) and any custom roles with non-admin names (e.g. dev-team, ci-runner, viewer-plus) that appear here.
    • Any role clearly intended for application workloads, CI/CD, or non-ops users.

# 2) Inspect each suspicious ClusterRole in detail
# Replace <CLUSTERROLE_NAME> with a name from the previous output
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml

What to look for

  • Problematic if:
    • resources includes validatingwebhookconfigurations and verbs includes powerful verbs (create, update, patch, delete, deletecollection, or *).
    • apiGroups is admissionregistration.k8s.io with broad wildcard usage:
      • resources: ["*"] or verbs: ["*"].
  • Acceptable only for tightly controlled admin/cluster-operations roles.

# 3) List Roles (namespace-scoped) with access to validating webhooks (should normally be none)
kubectl get role --all-namespaces -o json \
| jq -r '
.items[]
| select(
(.rules[]?
| select(
(.resources[]? | IN("validatingwebhookconfigurations")) and
(.verbs[]? | IN("create","update","patch","delete","deletecollection","*"))
)
)?
)
| .metadata.namespace + ":" + .metadata.name
' | sort -u

What to look for

  • In most clusters, this should return no results.
  • Any Role here is suspicious, especially in application namespaces (e.g. default, dev, staging, prod).

# 4) Repeat for mutating webhook configurations: ClusterRoles
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
(.rules[]?
| select(
(.resources[]? | IN("mutatingwebhookconfigurations")) and
(.verbs[]? | IN("create","update","patch","delete","deletecollection","*"))
)
)?
)
| .metadata.name
' | sort -u
# 5) Inspect each suspicious ClusterRole for mutating webhooks
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml

What to look for

  • Same criteria as validating webhooks: strong verbs on mutatingwebhookconfigurations or wildcard rules in admissionregistration.k8s.io.

# 6) Roles (namespace-scoped) with access to mutating webhooks
kubectl get role --all-namespaces -o json \
| jq -r '
.items[]
| select(
(.rules[]?
| select(
(.resources[]? | IN("mutatingwebhookconfigurations")) and
(.verbs[]? | IN("create","update","patch","delete","deletecollection","*"))
)
)?
)
| .metadata.namespace + ":" + .metadata.name
' | sort -u

What to look for

  • Again, expected result is usually no Roles.
  • Any Role in non-admin namespaces indicates a likely problem.

# 7) Identify who is bound to risky ClusterRoles
# Replace <CLUSTERROLE_NAME> with any role you identified as suspicious
kubectl get clusterrolebinding -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="<CLUSTERROLE_NAME>")
| .metadata.name as $b
| .subjects[]?
| [$b, .kind, .namespace // "", .name]
| @tsv
' | column -t

What to look for

  • Problematic if:
    • ServiceAccounts used by apps/CI/CD pipelines are bound.
    • End-user groups (e.g. devs, qa, viewers) are bound.
  • Prefer only tightly controlled admin groups or ops service accounts.

# 8) Identify who is bound to risky Roles (if any)
# Replace <ROLE_NAME> and <NAMESPACE> from step 3/6 output
kubectl get rolebinding -n <NAMESPACE> -o json \
| jq -r '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")
| .metadata.name as $b
| .subjects[]?
| [$b, .kind, .namespace // "", .name]
| @tsv
' | column -t

What to look for

  • Any non-admin users or application service accounts bound to these Roles are likely misconfigured.

Interpreting problems overall

  • A finding exists when:
    • Any non-admin or broad/shared roles provide create, update, patch, delete, deletecollection, or * on:
      • validatingwebhookconfigurations
      • mutatingwebhookconfigurations
    • And those roles are bound to:
      • Application/CI service accounts,
      • General developer/QA/user groups,
      • Or any subjects beyond a small, trusted administrative set.

Deciding which roles/subjects must retain this access and how to scope or remove it is a policy decision and must be done manually.

Automation
#!/usr/bin/env bash
#
# Report Roles/ClusterRoles that can access validating/mutating webhook configurations
# Run on: any machine with kubectl access and appropriate RBAC to list roles/bindings

set -euo pipefail

echo "=== Checking ClusterRoles with webhook configuration permissions ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
rules: .rules[]?
| select(.apiGroups[]? == "admissionregistration.k8s.io")
| select(.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
}
| select(.rules != null)
| .name as $name
| .rules
| [
$name,
(.verbs | join(",")),
(.resources | join(",")),
(.apiGroups | join(","))
]
| @tsv
' 2>/dev/null \
| awk 'BEGIN{FS="\t"; OFS="\t"; print "TYPE","NAME","VERBS","RESOURCES","APIGROUPS"}
{print "ClusterRole",$0}'

echo
echo "=== Checking Roles with webhook configuration permissions (namespace-scoped) ==="
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| .metadata.namespace as $ns
| .metadata.name as $name
| {
ns: $ns,
name: $name,
rules: .rules[]?
| select(.apiGroups[]? == "admissionregistration.k8s.io")
| select(.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
}
| select(.rules != null)
| .ns as $ns
| .name as $name
| .rules
| [
$ns,
$name,
(.verbs | join(",")),
(.resources | join(",")),
(.apiGroups | join(","))
]
| @tsv
' 2>/dev/null \
| awk 'BEGIN{FS="\t"; OFS="\t"; print "TYPE","NAMESPACE","NAME","VERBS","RESOURCES","APIGROUPS"}
{print "Role",$0}'

echo
echo "=== Showing who can use these ClusterRoles (ClusterRoleBindings) ==="
# Find ClusterRoles that touch webhook configs, then show their bindings
bad_crs=$(kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
[.rules[]?
| select(.apiGroups[]? == "admissionregistration.k8s.io")
| select(.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
] | length > 0
)
| .metadata.name
')

if [ -n "${bad_crs}" ]; then
kubectl get clusterrolebindings -o json \
| jq -r --argjson bad "[$(printf '"%s",' ${bad_crs} | sed 's/,$//')]" '
.items[]
| . as $crb
| select(.roleRef.kind == "ClusterRole" and (.roleRef.name as $rn | $bad | index($rn)))
| (
"ClusterRoleBinding: " + .metadata.name,
" RoleRef: " + .roleRef.kind + "/" + .roleRef.name,
" Subjects:",
( .subjects[]?
| " - " + (.kind // "") + "/" + (.namespace // "") + ":" + (.name // "")
),
""
)
'
else
echo "No ClusterRoles with webhook configuration permissions found."
fi

echo
echo "=== Showing who can use these Roles (RoleBindings) ==="
# Find Roles that touch webhook configs, then show their bindings
bad_roles=$(kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| select(
[.rules[]?
| select(.apiGroups[]? == "admissionregistration.k8s.io")
| select(.resources[]? | IN("validatingwebhookconfigurations","mutatingwebhookconfigurations"))
] | length > 0
)
| [.metadata.namespace, .metadata.name]
| @tsv
')

if [ -n "${bad_roles}" ]; then
# Build a jq filter that matches any (namespace,name) pair
jq_filter='
.items[]
| . as $rb
| select(.roleRef.kind == "Role")
| .roleRef.name as $rname
| .metadata.namespace as $rbns
'
# Post-filter in shell since jq can't easily take a big set of (ns,name) pairs
kubectl get rolebindings --all-namespaces -o json \
| jq -r "${jq_filter} | [\$rbns, \$rname] | @tsv" \
| while IFS=$'\t' read -r rb_ns rb_role; do
if printf '%s\n' "${bad_roles}" | grep -q -E "^${rb_ns}[[:space:]]+${rb_role}\$"; then
kubectl get rolebinding -n "${rb_ns}" "$(kubectl get rolebinding -n "${rb_ns}" -o json \
| jq -r --arg ns "${rb_ns}" --arg rn "${rb_role}" '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name==$rn and .metadata.namespace==$ns)
| .metadata.name
')" -n "${rb_ns}" -o yaml
fi
done
else
echo "No Roles with webhook configuration permissions found."
fi

Interpretation guidance (what indicates a problem):

  • Any Role/ClusterRole listed with:
    • RESOURCES including validatingwebhookconfigurations or mutatingwebhookconfigurations, and
    • VERBS including create, update, patch, or delete
  • Especially problematic if the associated (Cluster)RoleBindings show:
    • system:authenticated, system:serviceaccounts, or broad groups rather than a small set of named admin users/groups.
  • Acceptable patterns typically:
    • Are limited to a small, clearly-admin ClusterRole (e.g. cluster-admin in some environments) bound only to a trusted admin group, or
    • Are used by tightly-controlled CI/CD or operator service accounts with a justified need.