Securing the Kubernetes API Aggregation Layer Against Privilege Escalation
Problem
The Kubernetes API aggregation layer allows operators to register extension API servers that appear as native Kubernetes API groups. Clients interact with these extension APIs through the kube-apiserver, which proxies requests after performing authentication and attaching the caller’s identity in HTTP headers. From the outside, aggregated APIs look like any other Kubernetes API — they respond to kubectl, appear in API discovery, and support the same RBAC verbs.
The problem is structural. The kube-apiserver trusts extension API servers with the full caller identity. When proxying a request, it forwards the original bearer token (or injects the user identity via X-Remote-User and X-Remote-Group headers), and the extension API server is expected to enforce its own authorization. If that extension API server has a vulnerable authorization model, ignores the forwarded identity, or is itself compromised, the attacker gains control of an API surface that the cluster’s RBAC rules treat as authoritative.
Multiple attack paths exist:
Token interception. The kube-apiserver forwards the original bearer token when communicating with extension API servers unless --requestheader-allowed-names and the requestheader CA are configured correctly. A misconfigured extension API server can log or exfiltrate service account tokens, kubeconfig credentials, or user OIDC tokens that flow through it.
RBAC gap via APIService registration. An attacker who can create or modify an APIService object (the CRD that registers an aggregated API) can redirect requests for an existing API group to a malicious endpoint. The attacker needs create or update on apiregistration.k8s.io/v1/apiservices — a permission that is overly broad in many cluster roles.
Privilege escalation via extension API server misconfiguration. Many extension API servers (metrics-server, custom controllers, operators) are deployed with cluster-admin equivalent permissions because their authors didn’t scope RBAC carefully. A vulnerability in the extension server itself gives the attacker those permissions.
RequestHeader credential forgery. If the extension API server does not strictly validate that requests arrive from the kube-apiserver (by verifying the client certificate against the requestheader CA), it can be called directly by an attacker who constructs arbitrary X-Remote-User headers to impersonate any identity.
Recent disclosure: CVE-2025-1974 (ingress-nginx) was partly enabled by the fact that the ingress-nginx admission webhook — functionally similar to an aggregated API — had direct access to the kube-apiserver credentials in its pod. The same structural pattern applies to many aggregated API servers. Earlier, CVE-2022-3172 in kube-aggregator allowed a malicious aggregated API server to perform SSRF against cluster-internal endpoints.
The aggregation layer is underaudited because it is rarely thought of as an attack surface. Teams deploy metrics-server, cert-manager, and custom operators through Helm charts, accept the default RBAC, and never revisit it. The result is an expanding set of extension API endpoints with elevated cluster permissions and weak requestheader validation.
Target systems: Kubernetes 1.24–1.32 on all managed (EKS, GKE, AKS) and self-managed distributions; any cluster with --enable-aggregator-routing or installed aggregated API servers (metrics-server, custom resource APIs, service catalog, OpenAPI v3 extensions).
Threat Model
Adversary 1 — Namespace-scoped attacker with APIService write. Access level: service account with create apiservices in apiregistration.k8s.io. Objective: register a malicious APIService that routes v1beta1.metrics.k8s.io to an attacker-controlled pod, intercepting node/pod metrics API calls and extracting any tokens forwarded through them.
Adversary 2 — Compromised extension API server pod. Access level: code execution inside an existing extension API server pod (e.g., via a supply chain compromise or container escape from a co-located workload). Objective: read ServiceAccount tokens mounted in the pod, which often carry cluster-admin equivalent RBAC, then call the kube-apiserver directly to enumerate secrets or escalate.
Adversary 3 — Direct requestheader call. Access level: network access to an extension API server’s service port (achievable from any pod in the cluster). Objective: construct an HTTP request with forged X-Remote-User: kubernetes-admin and X-Remote-Group: system:masters headers to authenticate as a cluster superuser if the extension server doesn’t validate the client certificate.
Adversary 4 — APIService redirect via supply chain. Access level: control over a Helm chart update or GitOps repository. Objective: modify an APIService endpoint to point to a malicious pod in a compromised namespace.
Without hardening, a single compromised extension API server or a single misconfigured APIService can result in full cluster compromise. With hardening, requestheader credential forgery is blocked by mutual TLS, APIService modifications require elevated approval, and extension servers run with minimal RBAC.
Configuration / Implementation
Step 1 — Audit existing APIService objects and their endpoints
# List all registered APIService objects and their backing services
kubectl get apiservices -o custom-columns=\
"NAME:.metadata.name,SERVICE:.spec.service,\
CABUNDLE:.spec.caBundle,INSECURE:.spec.insecureSkipTLSVerify"
# Find any APIServices using insecureSkipTLSVerify=true (critical misconfiguration)
kubectl get apiservices -o json | \
jq '.items[] | select(.spec.insecureSkipTLSVerify == true) | .metadata.name'
# Find APIServices with no caBundle (unverified TLS)
kubectl get apiservices -o json | \
jq '.items[] | select(.spec.caBundle == null and .spec.service != null) | .metadata.name'
Any APIService with insecureSkipTLSVerify: true is directly vulnerable to MITM. Any with no caBundle cannot cryptographically verify the extension server’s identity.
Step 2 — Enforce TLS verification on all APIService objects
For each extension API server, generate or obtain its serving certificate and configure the CA bundle in the APIService:
# Extract the CA cert from the extension server's serving cert secret
kubectl get secret metrics-server-tls -n kube-system \
-o jsonpath='{.data.ca\.crt}' | base64 -d > /tmp/metrics-ca.crt
# Encode it
CA_BUNDLE=$(base64 -w 0 /tmp/metrics-ca.crt)
# Patch the APIService to remove insecureSkipTLSVerify and add caBundle
kubectl patch apiservice v1beta1.metrics.k8s.io --type=merge -p "{
\"spec\": {
\"insecureSkipTLSVerify\": false,
\"caBundle\": \"${CA_BUNDLE}\"
}
}"
For metrics-server specifically, deploy with TLS verification enabled:
# metrics-server deployment args — add TLS cert generation
containers:
- name: metrics-server
args:
- --cert-dir=/tmp
- --secure-port=10250
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
# Do NOT use --kubelet-insecure-tls in production
Step 3 — Validate requestheader client certificate in extension API servers
Extension API servers must verify that requests claiming to come from the kube-apiserver carry a valid client certificate signed by the requestheader CA.
Configure the requestheader CA in the kube-apiserver:
# Confirm these flags are set on the kube-apiserver
grep -E "requestheader-(client-ca-file|allowed-names|username-headers|group-headers)" \
/etc/kubernetes/manifests/kube-apiserver.yaml
Expected output:
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-allowed-names=front-proxy-client
- --requestheader-username-headers=X-Remote-User
- --requestheader-group-headers=X-Remote-Group
- --requestheader-extra-headers-prefix=X-Remote-Extra-
In each extension API server, configure client certificate validation using the same front-proxy CA. For Go-based extension servers using k8s.io/apiserver:
// In the extension server's main.go or config setup
options.Authentication.RequestHeader.ClientCAFile = "/etc/kubernetes/pki/front-proxy-ca.crt"
options.Authentication.RequestHeader.AllowedNames = []string{"front-proxy-client"}
options.Authentication.RequestHeader.UsernameHeaders = []string{"X-Remote-User"}
options.Authentication.RequestHeader.GroupHeaders = []string{"X-Remote-Group"}
For extension servers you do not control the source of, verify the Helm chart configures the same CA:
# Find the front-proxy CA
kubectl get configmap extension-apiserver-authentication -n kube-system \
-o jsonpath='{.data.requestheader-client-ca-file}'
The extension-apiserver-authentication ConfigMap contains the front-proxy CA that all well-configured extension servers should use.
Step 4 — Restrict RBAC on APIService management
Creating or modifying an APIService is a high-privilege operation. Lock it down:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: apiservice-reader
rules:
- apiGroups: ["apiregistration.k8s.io"]
resources: ["apiservices"]
verbs: ["get", "list", "watch"]
# Deliberately excludes: create, update, patch, delete
---
# Remove APIService write from any non-admin ClusterRoleBindings
# Audit who has create/update on apiservices:
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
. as $b |
$b.roleRef.name as $role |
$b.subjects[]? |
"\($b.metadata.name): \(.kind)/\(.name)"
'
# Find ClusterRoles that permit apiservice modification
kubectl get clusterroles -o json | jq -r '
.items[] |
select(.rules[]? |
(.apiGroups[]? | contains("apiregistration.k8s.io")) and
(.verbs[]? | test("create|update|patch|delete"))
) | .metadata.name
'
Step 5 — Apply minimal RBAC to extension API servers
Audit the ServiceAccount used by each extension API server and reduce its permissions:
# Find ClusterRoleBindings for metrics-server
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.subjects[]?.name == "metrics-server") | .roleRef.name'
If it shows cluster-admin, replace with a scoped role:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-server-minimal
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "nodes/stats", "namespaces", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes/metrics"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-server-minimal
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-server-minimal
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
Step 6 — Network policy to restrict direct extension server access
Prevent pods from calling extension API server services directly, bypassing the kube-apiserver:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: metrics-server-ingress
namespace: kube-system
spec:
podSelector:
matchLabels:
app: metrics-server
policyTypes:
- Ingress
ingress:
- from:
# Only the kube-apiserver node IP range should reach this service
- ipBlock:
cidr: 10.0.0.0/16 # Replace with your control-plane CIDR
ports:
- port: 10250
protocol: TCP
Step 7 — Monitor APIService changes with audit policy
# kube-apiserver audit policy — high-value events for aggregation layer
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "apiregistration.k8s.io"
resources: ["apiservices"]
- level: Request
verbs: ["get", "list", "watch"]
resources:
- group: "apiregistration.k8s.io"
resources: ["apiservices"]
Alert on any APIService create/update outside of your CI/CD pipeline’s service account.
Expected Behaviour
| Signal | Before hardening | After hardening |
|---|---|---|
kubectl get apiservices shows insecureSkipTLSVerify: true |
Present on metrics-server and others | None; all have verified caBundle |
Direct HTTP call to extension server with forged X-Remote-User header |
Returns 200 as impersonated user | Returns 401 — client cert required |
Non-admin service account attempts kubectl create apiservice |
Succeeds if broad ClusterRoleBinding exists | Returns 403 |
| Audit log for APIService modification | No alert | Alert fires within 60 seconds |
Verification:
# Confirm no insecure APIServices remain
kubectl get apiservices -o json | \
jq '[.items[] | select(.spec.insecureSkipTLSVerify == true)] | length'
# Expected: 0
# Confirm requestheader CA is configured
kubectl get configmap extension-apiserver-authentication -n kube-system -o yaml | grep -c "requestheader"
# Expected: > 0
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Enforcing caBundle on APIServices | Prevents TLS MITM against extension servers | Requires managing CA certs for each extension server | Use cert-manager to automate serving cert rotation; update caBundle via annotation-driven rotation |
| Restricting APIService write RBAC | Prevents rogue API registration | Blocks operators from self-registering via Helm | Require APIService changes via GitOps with separate approver; document in runbook |
| Minimal RBAC on extension servers | Limits blast radius of compromised extension server | Helm chart defaults often require broad permissions; restricting may break features | Test in non-prod; diff Helm chart role against minimal role; file upstream issue |
| Network policy blocking direct extension server access | Prevents requestheader forgery from pods | May break legitimate monitoring or health-check paths | Add explicit exception for kube-apiserver node IPs and internal monitoring |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Extension server cert rotation breaks caBundle | kubectl top pods returns 503; API availability probe fails |
APIService Available condition goes False; kubectl get apiservice shows False |
Re-extract CA cert from extension server secret; re-patch caBundle; consider cert-manager with auto-sync |
| Minimal RBAC breaks extension server functionality | Extension server crashes or returns permission errors on startup | Extension server pod logs show 403 from kube-apiserver; RBAC audit log shows denials | Add the specific resource/verb pairs being denied; do not restore cluster-admin |
| Network policy blocks kube-apiserver health checks | APIService marked unavailable even though extension server is running | kubectl describe apiservice shows connection refused or timeout |
Add explicit ingress rule for kube-apiserver node CIDR; verify with kubectl auth can-i |
| Front-proxy CA mismatch | Extension server rejects all kube-apiserver proxy requests with 401 | All aggregated API calls return 401; extension server logs show “unknown client cert” | Ensure the CA in extension server matches extension-apiserver-authentication ConfigMap |
Related Articles
- Kubernetes RBAC Design Patterns — foundational RBAC scoping that prevents broad APIService write permissions from accumulating
- Kubernetes API Server Hardening — kube-apiserver flags including requestheader configuration
- Kubernetes Service Account Token Security — protecting the tokens that flow through aggregated API proxying
- Validating Admission Policy with CEL — enforce policy on APIService objects without a webhook
- SPIFFE/SPIRE Workload Identity — issuing verifiable identities to extension API servers so mutual TLS is automated