Kyverno is the tool most teams reach for to enforce Kubernetes least privilege: block privileged pods, require signed images, deny risky RBAC. So there's a particular irony in CVE-2026-4789. The policy engine you deployed to close namespace-isolation gaps became a way to walk straight through them. Per the GitHub Security Advisory (GHSA-rggm-jjmc-3394, published April 13, 2026), the flaw is a server-side request forgery in Kyverno's CEL HTTP library, carrying CVSS 8.5 and affecting versions 1.16.0 and later.
The shape of it is a textbook confused deputy. Kyverno's CEL policies can call http.Get() and http.Post(). Those functions, per the advisory, "do not enforce any URL restrictions." A user who can only create namespaced policy objects (NamespacedValidatingPolicy, NamespacedMutatingPolicy, and the rest) can embed an arbitrary URL in a policy expression. When that policy evaluates, the request originates not from the low-privilege user but from the Kyverno admission controller pod, which sits on the cluster network with broad reach. Kubernetes RBAC governs API calls. It does not govern raw outbound TCP from a pod. So the deputy makes the call the user never could.
If you already run Kyverno to block privileged pods with a ValidatingPolicy, this is the uncomfortable part: the same delegation model you trust for enforcement is the one that hands out the SSRF primitive.
Two CVEs, one design lesson
This isn't the only Kyverno namespace-isolation break disclosed this cycle. CVE-2026-22039, a separate bug in the apiCall context feature, is scored CVSS 10.0 by Minimus, who traced it to user-controlled annotations being substituted into a urlPath that Kyverno then fetches using its own admission-controller ServiceAccount. Same root pattern, different code path.
Both bugs come down to a library asymmetry, not a logic typo. Per Orca's analysis, Kyverno's resource.Lib enforces namespace boundaries. The http.Lib was initialized with no namespace parameter and no destination checks at all. The two libraries encode two different trust assumptions, and the HTTP one quietly assumed every policy author was a cluster admin. That assumption held right up until multi-tenancy made it false.
What an attacker actually reaches
The blast radius is what makes this worth acting on this week rather than filing for the next maintenance window. Orca Security, which reported the SSRF, demonstrated three concrete targets reachable from the admission controller's network position:
-
Cloud metadata at
169.254.169.254. Unauthenticated on IMDSv1. AWS IAM role keys sit at/latest/meta-data/iam/security-credentials/, GCP service-account tokens at/computeMetadata/v1/instance/service-accounts/, Azure managed-identity tokens at/metadata/identity/oauth2/token. Network position alone is the credential. -
Cross-namespace internal services. Anything resolvable at
*.svc.cluster.local, includingkube-systemservices a tenant namespace should never touch. -
Exfiltration through error messages. Orca's proof-of-concept smuggles the HTTP response body back out through the admission webhook's denial message:
admission webhook denied the request: SSRF_LEAKED: secret=STOLEN.... The stolen data never needs a separate channel. It rides the API rejection back to the caller.
That last one deserves a beat. Most SSRF write-ups assume the attacker needs a way to see the response. Here the response comes home for free, wrapped in the very rejection that looks, in your audit log, like the policy engine doing its job.
Who's exposed: any multi-tenant cluster that lets teams create their own Kyverno policies in their own namespaces, which is exactly the delegation model the namespaced policy types were built to enable. The people you trusted with only their namespace can now read your cloud credentials. On a young cluster with IMDSv1 still enabled and node roles scoped generously, that is game over, not a finding. Leaked node-role credentials are also the ugly kind to clean up, because you're into a full credential rotation across whatever those roles could touch.
The patched-version story is messier than one number
This is where I'd slow down before typing a helm upgrade. The fixed-version picture is not clean, and reading only the GitHub advisory will leave you short.
The GitHub advisory lists 1.17.0 as the fixed release. Orca reported no clean patched release as of their writeup and demonstrated the exploit on 1.16.2 (Helm chart 3.6.2). And byteiota reports that the complete set of controls, meaning HTTP restricted to cluster-scoped policies by default plus a default blocklist for loopback and metadata ranges, only lands in Kyverno 1.18, released May 5, 2026.
So treat 1.18 as the version that actually carries the full default-off behavior. Do not assume a 1.17.x bump alone hardens the defaults. Maintainer PR #15789 (merged to main April 6, 2026) is what inverts the default: it disables the HTTP library for namespaced policies unless you explicitly opt in with --allowHTTPInNamespacedPolicies, and it puts loopback (127.0.0.0/8) and link-local/metadata (169.254.0.0/16) on a default blocklist. Re-enable HTTP for namespaced policies and you re-open most of the hole.
CVE-2026-22039 has its own patch line. Minimus reports it fixed in Kyverno 1.16.3 and 1.15.3, affecting 1.16.2-and-earlier and 1.15.2-and-earlier. Because it's a different code path, the apiCall context rather than the CEL http lib, patching one does not patch the other. Check both against your running version.
One more number that should shape your urgency. Orca's timeline: reported January 29, 2026; CVE assigned March 24; interim mitigation PR #15729 on March 26; maintainer fix merged April 6. That is roughly a three-month window where the SSRF was live and unpatched. Assume exposure predates your awareness, and check logs accordingly rather than trusting that "we only just heard about it" means "we were only just exposed."
Upgrading fixes the default. It does not fix a malicious author.
Here's the honest framing, and the part that separates a patch from actual safety. The upgrade flips the default so a careless policy author can't wander into the SSRF. It does nothing about a policy author who is actively malicious or whose credentials are compromised, if that author can flip --allowHTTPInNamespacedPolicies back on or exploit a path you haven't patched.
Containment is a different control from prevention, and you want both. A default-deny egress NetworkPolicy on the Kyverno namespace would have starved this exploit even on a fully vulnerable build. It doesn't stop the initial policy from evaluating. It stops the packet from reaching 169.254.169.254, which is the difference between a leaked secret and a logged, blocked request. Network-layer containment is what saves you on the days your version pin is wrong, and given the version story above, some of your clusters are probably pinned wrong right now.
What to do before your next Kyverno upgrade window
Order matters here. Close the CVSS 10.0 first, then the SSRF, then the containment that protects you regardless.
- If you're on the 1.15/1.16 line, jump to 1.16.3 or 1.15.3 immediately to close CVE-2026-22039. It's CVSS 10.0 and a different code path from the SSRF, so this is its own step, not a side effect of the bigger upgrade.
- Upgrade to Kyverno 1.18 or later for the SSRF default-off behavior plus the metadata/loopback blocklist. Do not stop at 1.17.x and assume the defaults are hardened; byteiota puts the full default behavior in 1.18.
-
After upgrading, confirm you did not set
--allowHTTPInNamespacedPolicies. The flag exists precisely to re-enable the risky behavior. If a team genuinely needs HTTP in a namespaced policy, that's a review conversation, not a running default. -
Apply a default-deny egress NetworkPolicy to the Kyverno namespace and explicitly block
169.254.169.254at the network layer. This is the control that holds even on a vulnerable build. -
Enforce IMDSv2 on AWS nodes with
http-tokens: required. IMDSv2 demands a PUT-issued token before any credential read, which a naivehttp.Get()SSRF can't perform. That neutralizes the single highest-value SSRF target on AWS. -
Audit who can create namespaced policy objects. Run
kubectl get clusterrolebindings,rolebindings -A -o wideand check forcreateon theNamespaced*PolicyCRDs. Everyone on that list held the SSRF primitive on a vulnerable cluster. Scope it to the teams that truly need self-service policy. Tightening this also feeds directly into broader non-human identity governance, since the admission controller's ServiceAccount is exactly the kind of over-trusted workload identity that gets abused here.
While you're in the upgrade, watch one more clock. byteiota notes ClusterPolicy is slated for removal in v1.20 (October 2026) with no automated migration tool. The upgrade you're already doing for this CVE is the natural moment to start the manual split into ValidatingPolicy, MutatingPolicy, and the rest, rather than eating two disruptive Kyverno migrations in one quarter.
Sources
- GitHub Advisory GHSA-rggm-jjmc-3394 (CVE-2026-4789)
- Orca Security: Kyverno SSRF Vulnerability (CVE-2026-4789)
- Kyverno PR #15789: secure HTTP calls
- Minimus: CVE-2026-22039 Kyverno authorization bypass
- byteiota: Kyverno 1.18 SSRF patches and ClusterPolicy deprecation
Originally published at indragustiprasetya.com
Top comments (0)