GitHub Advisoryadvisory · Go

GHSA-fmqp-4wfc-w3v7 Kyverno APICall SSRF Vulnerability Leading to Multi-Tenant Isolation Breach

HIGH Published 2026-04-14 Modified 2026-04-14 Official page ↗
Ecosystem
Go
Packages
github.com/kyverno/kyverno
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Weaknesses
CWE-918
Reviewed
2026-04-14

Affected packages

• Go github.com/kyverno/kyverno: < 1.16.2 → fixed in 1.16.2

Description

Summary Kyverno's APICall feature contains a Server-Side Request Forgery (SSRF) vulnerability that allows users with Policy creation permissions to access arbitrary internal resources through Kyverno's high-privilege ServiceAccount. In multi-tenant Kubernetes environments, this constitutes a classic Confused Deputy problem: low-privilege tenants can steal sensitive data from other tenants (such as database passwords and API keys) and cloud platform IAM credentials, completely breaking tenant isolation. This vulnerability does not require cluster-admin privileges and can be exploited with only namespace-level Policy creation permissions. Details Vulnerability Mechanism Kyverno's APICall feature allows Policies to fetch external data via HTTP requests. This feature does not validate target URLs when executing HTTP requests, leading to an SSRF vulnerability. Source Point - User-Controlled URL File: `api/kyverno/v1/common_types.go`, lines 247-250 ```go type ServiceCall struct { // URL is the JSON web service URL URL string `json:"url"` // User-controlled, no validation Headers []HTTPHeader `json:"headers,omitempty"` CABundle string `json:"caBundle,omitempty"` } ``` The URL field is completely controlled by users through Policy configuration, with no validation mechanism to restrict target addresses. Sink Point - HTTP Request Execution File: `pkg/engine/apicall/executor.go`, lines 65-110 ```go func (a *executor) executeServiceCall(ctx context.Context, apiCall *kyvernov1.APICall) ([]byte, error) { if apiCall.Service == nil { return nil, fmt.Errorf("missing service for APICall %s", a.name) } client, err := a.buildHTTPClient(apiCall.Service) if err != nil { return nil, err } req, err := a.buildHTTPRequest(ctx, apiCall) if err != nil { return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.name, err) } // Line 80: Directly executes HTTP request without URL validation resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute HTTP request for APICall %s: %w", a.name, err) } defer resp.Body.Close() // Read and return response content body, err := io.ReadAll(resp.Body) if err != nil { // ... } return body, nil } ``` Line 80's `client.Do(req)` directly executes the HTTP request without checking if the target URL is an internal IP address (like 169.254.169.254) or resources belonging to other tenants. Confused Deputy Problem In multi-tenant environments, Kyverno uses a cluster-wide high-privilege ServiceAccount to execute all APICall requests. When a low-privilege tenant creates a Policy containing malicious APICall directives, Kyverno executes these requests with its own high privileges, leading to privilege escalation. Attack path: ``` Tenant A (namespace-level permissions) → Creates malicious Policy → Kyverno (cluster-wide high privileges) → Accesses Tenant B's Secrets / Cloud metadata service → Sensitive data leaked to PolicyReport → Tenant A reads PolicyReport to obtain data ``` PoC Environment Setup Prerequisites • Kubernetes cluster • Kyverno v1.16.0 installed • Mock cloud metadata service (optional, for testing cloud credential theft) Step 1: Install Kyverno ```bash kubectl create namespace kyverno kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.16.0/install.yaml kubectl wait --for=condition=Ready pods --all -n kyverno --timeout=300s ``` Verify installation: ```bash $ kubectl get pods -n kyverno NAME READY STATUS RESTARTS AGE kyverno-admission-controller-5c84845f5-28hz5 1/1 Running 0 2m kyverno-background-controller-59b7b8d686-7pqxl 1/1 Running 0 2m kyverno-cleanup-controller-5fd988d64f-nsgdb 1/1 Running 0 2m kyverno-reports-controller-546cb78fbc-2dd74 1/1 Running 0 2m ``` Step 2: Deploy Mock Metadata Service Create file `metadata-mock.yaml`: ```yaml apiVersion: v1 kind: Namespace metadata: name: metadata-mock --- apiVersion: apps/v1 kind: Deployment metadata: name: metadata-mock namespace: metadata-mock spec: replicas: 1 selector: matchLabels: app: metadata-mock template: metadata: labels: app: metadata-mock spec: containers: • name: mock-server image: python:3.9-slim command: ["python", "-c"] args: • | import http.server import socketserver import json class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self): if 'iam/security-credentials/test-role' in self.path: self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() creds = { "AccessKeyId": "AKIAIOSFODNN7EXAMPLE", "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "Token": "SimulatedSessionToken123456", "Expiration": "2025-12-31T23:59:59Z" } self.wfile.write(json.dumps(creds).encode()) else: self.send_response(404) self.end_headers() with socketserver.TCPServer(("", 80), Handler) as httpd: httpd.serve_forever() ports: • containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: metadata-mock namespace: metadata-mock spec: selector: app: metadata-mock ports: • port: 80 targetPort: 80 ``` Deploy: ```bash kubectl apply -f metadata-mock.yaml kubectl wait --for=condition=Ready pods --all -n metadata-mock --timeout=120s ``` Step 3: Create Multi-Tenant Environment Create two tenant namespaces: ```bash kubectl create namespace tenant-a kubectl create namespace tenant-b ``` Create sensitive data in tenant-b: ```bash kubectl create secret generic db-credentials -n tenant-b \ --from-literal=username=admin \ --from-literal=password=SuperSecret123! \ --from-literal=database=production-db ``` Create restricted ServiceAccount for tenant-a: ```bash kubectl create serviceaccount tenant-a-admin -n tenant-a ``` Create file `tenant-a-rbac.yaml`: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: policy-creator namespace: tenant-a rules: • apiGroups: "[kyverno.io"] resources: ["policies"] verbs: ["create", "get", "list", "update", "patch", "delete"] • apiGroups: [""] resources: ["configmaps"] verbs: ["create", "get", "list"] • apiGroups: "[wgpolicyk8s.io"] resources: ["policyreports"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tenant-a-policy-creator namespace: tenant-a roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: policy-creator subjects: • kind: ServiceAccount name: tenant-a-admin namespace: tenant-a ``` Apply configuration: ```bash kubectl apply -f tenant-a-rbac.yaml ``` Step 4: Verify Permission Isolation Create test Pod: ```bash kubectl run tenant-a-test -n tenant-a \ --image=bitnami/kubectl:latest \ --serviceaccount=tenant-a-admin \ --command -- sleep 3600 ``` Verify tenant-a cannot directly access tenant-b: ```bash $ kubectl exec -n tenant-a tenant-a-test -- kubectl get secrets -n tenant-b Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:tenant-a:tenant-a-admin" cannot list resource "secrets" in API group "" in the namespace "tenant-b" ``` This co…

Related

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References