Securing the Kubernetes API Aggregation Layer Against Privilege Escalation

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