Kyverno's apiCall feature is exactly the kind of capability platform teams end up wanting after the first wave of policy adoption.
Static admission checks are useful, but real clusters are full of context. A namespace may need to be compared against an inventory system. A deployment may need an exception lookup. An image policy may need external metadata. A policy decision may depend on something that is not inside the admission request itself.
That is the attraction of external data in a policy engine: the control plane can ask another system a question before it admits a resource.
The risk is the same sentence from the other direction: the control plane can ask another system a question before it admits a resource.
Recent Kyverno advisories around apiCall, CEL HTTP functions, service account token forwarding, and cross-namespace data access are worth reading as more than a set of individual CVEs. The common thread is a trust-boundary problem. A policy engine that can perform outbound HTTP from inside the cluster is no longer just evaluating policy. It is also acting as an egress client with control-plane credentials, control-plane network position, and admission-path availability requirements.
That changes how I would review it.
TL;DR
The Kyverno project published several 2026 advisories describing ways that policy-controlled HTTP or context-loading behavior could be abused. The themes include:
- server-side request forgery through policy-controlled URLs;
- non-blind SSRF where response data can influence or appear in policy outcomes;
- implicit forwarding of the Kyverno admission controller service account token on outbound HTTP calls;
- namespaced policy behavior crossing tenant or namespace boundaries;
- follow-on fixes in the 1.16, 1.17, and 1.18 release lines, including HTTP blocklist/allowlist controls and scoped token handling.
The exact version exposure depends on which Kyverno APIs and features are enabled in a cluster, so this is not a substitute for reading the advisories against your deployed version. The important operating principle is more durable: if policy authors can cause the policy engine to initiate network calls, then policy authoring is a form of egress authority.
Why this class of bug matters
SSRF in a normal application is already uncomfortable. SSRF in an admission controller is more interesting because the component sits in a privileged place by design.
Kyverno is not unusual here. Any serious admission controller or policy engine needs permissions to inspect resources, manage reports, serve webhooks, and interact with the Kubernetes API. Depending on installation and feature set, it may also need access to webhook configuration, generated resources, reports, image metadata, config maps, secrets, or other cluster objects. It runs on the cluster network. It is called during resource admission. It is usually managed by GitOps or a platform team rather than by each application team directly.
That combination creates a confused deputy shape:
- A user, tenant, or GitOps change controls some part of a policy or resource.
- Kyverno interprets that input while handling admission or policy evaluation.
- Kyverno makes a network request from its own network position.
- The target sees Kyverno, not the original user.
- The response or side effect is folded back into admission, reporting, or policy context.
If the URL is attacker-influenced, the attacker may get Kyverno to reach internal services they cannot reach directly. If a bearer token is attached, the attacker may receive a credential they should never see. If the response is reflected through an admission error or policy result, the SSRF stops being blind. If the policy is delivered by GitOps, then a repository write path may become a cluster-network request path.
None of that requires the policy engine to be poorly designed in general. It is a natural failure mode when a powerful control-plane component is given a flexible network primitive without equally strong destination controls.
The useful feature that creates the boundary
Kyverno documents external data sources as a way to load context into policy evaluation. A policy can call the Kubernetes API through urlPath, or call another service through a service URL. The current documentation also states that service calls receive JSON responses and, when a policy does not explicitly provide an Authorization header, Kyverno uses a scoped service account token for outbound HTTP context requests.
That last detail is important because it shows the project has moved toward a safer model: scoped token handling, blocklists, allowlists, and defaults that are meant to reduce replay and destination risk. Kyverno 1.18 release notes call out secure HTTP calls with blocklist/allowlist support and scoped token authorization as highlights.
But operators still have to treat the feature as a boundary. The question is not only, "Is the current Kyverno version patched?" It is also:
- Who can define or modify policies that contain outbound calls?
- Can namespaced users influence HTTP destinations, headers, bodies, or CEL expressions?
- Can request object fields be substituted into a URL?
- Can Kyverno reach cloud metadata endpoints, internal admin services, service meshes, or tenant services?
- What credentials are attached to those requests?
- Where would we see evidence if this path were abused?
GitOps changes the threat model
A team may be allowed to propose policy changes through a repository. A platform repository may be protected, but still writable by automation. A CI job may template policies from values files. A break-glass process may allow direct changes during an incident. A namespace admin may be allowed to manage namespaced policy objects, depending on how the policy engine has been introduced to tenants.
Those paths are often reviewed as configuration risk. For policy engines with outbound HTTP, they are also network and credential risk.
A malicious or compromised policy change does not need to look like a shell command. It may look like a validation rule that calls an external service. It may look like a variable substitution in a URL. It may look like a GlobalContextEntry that fetches data. It may look like an exception mechanism that everyone expects to be dynamic because the organization asked for more flexible policy.
That is why I would not stop at "only platform engineers can edit ClusterPolicies." I would map the actual write paths:
- direct Kubernetes RBAC for
ClusterPolicy,Policy, and related Kyverno policy APIs; - GitHub, GitLab, or Azure DevOps permissions on the repos that render policy;
- CI/CD identities that can apply policy objects;
- Argo CD or Flux projects that are allowed to sync policy namespaces;
- exception workflows that can introduce new context sources or bypass controls.
In a GitOps cluster, repository access is often cluster access with extra steps. For policy engines, repository access may also be egress authority with extra steps.
What I would check first
I would start with a boring inventory. Boring is good here. You want to know whether the vulnerable feature is present before debating exploitability.
kubectl get clusterpolicy,policy -A -o yaml > kyverno-policies.yaml
kubectl get globalcontextentry -A -o yaml > kyverno-global-context.yaml 2>/dev/null || true
grep -nE 'apiCall:|service:|url:|urlPath:|http\.Get|http\.Post|GlobalContextEntry|configMap:' \
kyverno-policies.yaml kyverno-global-context.yaml
That is intentionally simple. It will produce false positives. For a first pass, false positives are acceptable. The goal is to find every place policy evaluation can leave the object being admitted and fetch additional context.
Then I would answer four questions.
1. Which Kyverno version and release line is actually running?
Do not rely on the Helm chart version in a repo unless you have verified the deployed pods. Check the image tag and the controller arguments from the live cluster.
kubectl -n kyverno get deploy -o wide
kubectl -n kyverno get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'
kubectl -n kyverno get deploy kyverno-admission-controller -o yaml | grep -E 'httpAllowlist|httpBlocklist|allowHTTPInNamespacedPolicies|image:'
The command names may vary by installation, especially across Kyverno releases, but the point is to inspect the running admission controller and not just the desired state.
2. Who can create the inputs?
For cluster-scoped policy objects, check direct RBAC and GitOps ownership. For namespaced policy APIs, be more suspicious. A feature that is safe enough for a cluster platform team may be unsafe when delegated to namespace operators.
kubectl auth can-i create clusterpolicy --as system:serviceaccount:some-namespace:some-sa
kubectl auth can-i create policy -n some-namespace --as system:serviceaccount:some-namespace:some-sa
kubectl auth can-i create globalcontextentry --as system:serviceaccount:some-namespace:some-sa
Replace the service accounts with the identities that matter in your environment: CI runners, GitOps controllers, tenant automation, and platform break-glass users.
3. Where can Kyverno send traffic?
RBAC does not restrict egress. If the Kyverno pod can reach the Kubernetes API, internal services, cloud metadata, service mesh admin ports, or arbitrary internet destinations, then a policy-controlled HTTP primitive has room to move.
A NetworkPolicy is not a complete answer by itself, but it is a useful guardrail when the CNI enforces egress policy. A restrictive shape looks like this, with environment-specific destinations filled in deliberately:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: kyverno-egress-approved-destinations
namespace: kyverno
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: kyverno
policyTypes:
- Egress
egress:
# Kubernetes API server destination is cluster-specific.
- to:
- ipBlock:
cidr: 10.0.0.1/32
ports:
- protocol: TCP
port: 443
# DNS, if the controller needs it.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
# Approved in-cluster policy extension service, if used.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform-policy
podSelector:
matchLabels:
app: kyverno-extension
ports:
- protocol: TCP
port: 8443
Do not paste that into production as-is. The API server address, DNS path, labels, and extension service will differ. The important part is the operating model: default-deny egress for the policy engine, then explicitly allow the destinations that are part of the platform design.
4. What credentials leave the pod?
The older advisory pattern around implicit bearer token forwarding is a good reminder that "internal HTTP call" and "credentialed HTTP call" are different risks. If the request includes a token, the receiving service has to be trusted with that token. If the destination can be influenced, token forwarding becomes credential exfiltration.
For patched Kyverno versions, review the current scoped token behavior and the intended audience. For older versions, do not assume the default projected service account token is harmless just because Kubernetes has improved token handling over time. Check what the Kyverno service account can actually do.
kubectl auth can-i --list --as system:serviceaccount:kyverno:kyverno-admission-controller
The service account name may differ, but the question is simple: if that token were exposed, what would it buy?
The patch is necessary, not sufficient
Upgrading is still the first recommendation. Kyverno 1.16.4, 1.17.2, and 1.18.0 show up across the advisories and release notes as relevant fixed or hardened versions, depending on the specific issue. As usual, validate against the exact advisory and your release line before declaring a cluster clean.
After the upgrade, I would still keep the controls explicit:
- Use Kyverno's HTTP destination controls, including blocklists and allowlists, instead of relying on convention.
- Leave HTTP from namespaced policies disabled unless there is a strong reason and compensating egress control.
- Prefer
urlPathfor Kubernetes API lookups where appropriate, because it keeps the trust boundary clearer than arbitrary service URLs. - Keep policy engine egress behind NetworkPolicy or equivalent CNI controls.
- Limit who can create or update policies that perform external calls.
- Review GitOps repository permissions as part of the same control, not as a separate paperwork exercise.
- Monitor Kyverno egress with flow logs, DNS logs, or CNI observability where available.
The hard part is cultural, not technical. Platform teams often treat policy as safer than code because it is declarative. That is true for many policies, but not for all policy features. A rule that says "deny privileged containers" is a declarative guardrail. A rule that can make an HTTP request based on admission input is also a tiny integration running inside your control plane.
Those two things deserve different review paths.
A safer pattern for external context
There are legitimate reasons to use external context in admission. I would not ban the pattern outright. I would narrow it.
A safer design is usually:
- one or a small number of approved in-cluster policy extension services;
- stable service DNS names, not request-derived URLs;
- explicit authentication between Kyverno and the extension service;
- no forwarding of broad controller credentials to arbitrary destinations;
- short timeouts and clear failure behavior;
- observability on request volume, latency, errors, and denied destinations;
- a documented owner for the service, because admission-path dependencies need an owner.
This is the same lesson admission webhook outages teach from the availability side: once a dependency is in the admission path, it is part of the control plane whether or not it lives in the kube-system namespace.
External policy lookups are no different. They need a dependency contract. What happens if the service is down? What happens if it is slow? What happens if it returns malformed JSON? What happens if a tenant tries to make the policy call a different host? What happens if DNS is compromised? What happens if the extension service starts logging authorization headers?
If those questions feel heavy for a policy feature, that is the point. The feature is powerful enough to deserve the weight.
How I would frame the remediation ticket
For a real platform backlog, I would avoid writing this as "fix Kyverno CVEs" and then closing it after the version bump. I would split it into a short sequence:
- Upgrade Kyverno to a fixed release line for the relevant advisories.
- Inventory external context use:
apiCall, service URLs, CEL HTTP functions, GlobalContextEntry, and cross-namespace context loaders. - Restrict authoring: confirm only trusted platform identities can create cluster-scoped external-call policies, and avoid delegating HTTP-capable policy APIs to tenants.
- Constrain egress: NetworkPolicy or CNI egress controls for Kyverno pods, plus Kyverno HTTP blocklist/allowlist settings.
- Review credentials: service account RBAC, token audience, explicit auth headers, and whether any receiving service logs secrets.
- Add detection: DNS/flow visibility for Kyverno egress and alerts for denied destinations or unexpected external hosts.
- Document the contract: approved external policy services, failure behavior, owners, and review requirements for new external calls.
That gives the team a durable closure condition. The cluster is not merely patched; the policy engine has a defined egress boundary.
The broader lesson
Kubernetes keeps pushing more intelligence into admission, policy, and controllers because that is where platform teams can make consistent decisions. That is useful. It is also why these components accumulate surprising authority.
Kyverno's external data features are not inherently bad. They are a practical answer to a real platform need. But every time a control-plane component learns how to call out, fetch context, transform input, or attach credentials, the review model has to change with it.
For me, the lesson from the Kyverno advisories is this: policy engines need egress boundaries too.
Not because every policy author is malicious. Not because every external lookup is dangerous. Because the admission controller is a deputy. If you let other people or systems hand it destinations, headers, and request data, you have to decide where that deputy is allowed to go before the next advisory decides for you.
References
- Kyverno advisory GHSA-8wfp-579w-6r25: apiCall automatically forwards ServiceAccount token to external endpoints
- Kyverno advisory GHSA-qr4g-8hrp-c4rw: unrestricted outbound requests in apiCall enable non-blind SSRF
- Kyverno advisory GHSA-rggm-jjmc-3394 / CVE-2026-4789: CEL HTTP SSRF in namespaced policies
- Kyverno advisory GHSA-cvq5-hhx3-f99p: cross-namespace read bypass through context loading
- Kyverno v1.18.0 release notes
- Kyverno documentation: External Data Sources
- Kyverno documentation: installation customization and HTTP controls
- Kubernetes documentation: RBAC good practices
- Kubernetes documentation: Network Policies
- Kubernetes documentation: ServiceAccount administration