Privilege Escalation Paths
Documented routes from a single permission to a broken security boundary. Each path states the tier it reaches, which is not always T0.
Send Impersonate-User: cluster-admin header to the API server
API server evaluates all RBAC as the impersonated user
Read all secrets or perform any cluster-admin action
Send Impersonate-User together with Impersonate-Group: system:masters.
Kubernetes rejects group impersonation without an impersonated user.
The API server authorizes the request as a member of system:masters, a built-in group with unrestricted API-server access that bypasses RBAC/Webhook restrictions.
Perform any cluster-wide action.
Rights needed:
· both; the API server rejects a group header with no user header
Note:
In v1.36+, constrained impersonation may require action-scoped permissions for new-style grants, but legacy impersonate RBAC rules still work.
Mitigation is to remove the RBAC grant, fix IdP/IAM mapping, or rotate the CA only if the source is a leaked non-revokable system:masters client certificate.
Impersonate system:serviceaccount:kube-system:clusterrole-aggregation-controller
That SA has escalate on all ClusterRoles by default
Patch own bound ClusterRole to inject */* rules via impersonation
Use the escalate verb to bypass privilege-escalation prevention
Patch own bound ClusterRole to add */* rules (must be a role already bound to you)
Permissions expand via the existing binding.
No bind is needed when patching an already-bound ClusterRole.
Creating a new role and self-binding it requires bind clusterroles separately
Rights needed, any one set:
· escalate plus a way to write the rules
· editing an already-bound role needs no new binding
Note:
This is a by-design capability, not a CVE.
The escalate verb bypasses privilege-escalation prevention, so any identity holding it can grant permissions it does not already have.
No patch exists.
Use escalate on roles to bypass the check that normally prevents writing permissions you do not already hold.
Create or update a Role granting arbitrary permissions in the namespace.
Activate it with a RoleBinding.
Creating or changing the binding still needs bind on the referenced Role, or the caller must already hold the referenced permissions.
Rights needed, any one set:
· all four; escalate writes the rules, bind permits referencing them
· editing an existing Role instead of creating one
The bind verb bypasses escalation prevention on ClusterRoleBindings
Create a ClusterRoleBinding from cluster-admin to your own identity
You now hold cluster-admin
Rights needed:
· both; bind waives the check, create makes the binding
Note:
By design, holding bind on clusterrolebindings intentionally allows self-binding to cluster-admin.
No patch exists.
A RoleBinding can reference a ClusterRole.
This is valid RBAC.
The ClusterRole's permissions are scoped to the RoleBinding's namespace.
Cluster-scoped resources remain inaccessible.
bind on clusterroles bypasses the escalation check.
Create a RoleBinding to cluster-admin or any ClusterRole you don't fully hold.
Full control over namespace-scoped resources in that namespace, but cluster-scoped resources remain blocked.
This reaches T0 in kube-system via controller SA tokens and Secrets.
Rights needed:
· both; the RoleBinding may reference a ClusterRole
Note:
kubectl auth can-i falsely returns yes for cluster-scoped resources when bound via a RoleBinding to a wildcard ClusterRole (SubjectAccessReview quirk).
Actual requests are correctly denied.
A create or patch on nodes returns Forbidden.
Patch the subjects array of an existing ClusterRoleBinding
Kubernetes applies the same escalation check as CREATE, requiring bind on the referenced ClusterRole or already holding all its permissions
Useful for maintenance if you already hold bind.
It is not an independent bypass of the escalation check
Create or update a ClusterRole with an aggregation label such as rbac.authorization.k8s.io/aggregate-to-admin: true.
The ClusterRole aggregation controller merges its rules into the built-in admin/edit/view role.
Prevention applies at write time.
Without escalate, only rules the caller holds can be injected.
With escalate, any rules can be injected regardless of caller permissions.
All principals bound to the role gain injected permissions.
T0 only where admin/edit is cluster-bound or the namespace holds cluster-admin credentials.
Rights needed, any one set:
· escalate plus a way to write the labelled role
· relabelling an existing role also works
Note:
Escalation prevention blocks injection of rules the caller does not hold, even with an aggregate-to-admin label.
The escalate verb is required to inject arbitrary rules.
Without it, only rules the caller already possesses can be aggregated.
Identify a running pod you have patch access to, ideally one already carrying a privileged or high-value ServiceAccount.
Patch spec.containers[*].image.
Container images are among the few pod spec fields Kubernetes allows mutating after creation.
patch alone is enough.
A raw JSON Patch naming the container index needs no read, while kubectl patch additionally needs get because it fetches current state to compute the merge.
The kubelet detects the definition change, kills the container, and restarts it running the new attacker-controlled image.
The new container inherits the pod's existing ServiceAccount token, mounted volumes, and any host namespaces or capabilities already granted.
Note:
The API requires only patch.
A JSON Patch against a known container index needs no get, which kubectl needs purely for its client-side merge.
The kubelet restarts the container within seconds.
NetworkPolicy podSelector matches on a running Pod's labels, re-evaluated continuously, not fixed at creation
Patch the label a restrictive NetworkPolicy selects on to a value outside its selector
The same running Pod, same IP, same containers, is no longer covered by that policy's ingress/egress rules
Traffic that was blocked moments earlier now flows, with no image or spec change at all
Patch a NoExecute toleration onto an already-running Pod.
Tolerations are one of the few pod spec fields addable after creation
An incident responder later taints the suspect node NoExecute to evict everything running on it
The taint manager evicts every Pod without a matching toleration.
The pre-patched Pod is skipped and keeps running
A backdoor Pod outlives the exact response action meant to remove it from the node
Create a pod with privileged + hostPID or hostPath mounting /
Set spec.nodeName to a specific control plane node to force placement there.
Taints are scheduler-only checks, so no toleration is needed
chroot or nsenter into the host to gain root on the node
Read /etc/kubernetes/pki, steal kubelet cert or SA tokens.
This is T0 on the control plane
Rights needed, any one set:
Create a pod using hostNetwork, hostPID, hostIPC or hostPath as available
hostNetwork reaches cloud metadata and the API server from the node network namespace.
hostPID reads host process env vars
hostPath to / reads other pods' SA tokens under /var/lib/kubelet and the cluster CA private key if scheduled on a control plane node
Note:
On a control plane node, hostPath to / exposes the cluster CA private key for full cluster compromise.
On worker nodes this is node-level only (T1).
Compromise a scheduler component, built-in or custom, whose ServiceAccount holds create on pods/binding but not pods itself, matching the real system:kube-scheduler role
RBAC cannot filter by a Pod's schedulerName field, so any Pod already Pending in the cluster is fair game, not only ones addressed to that scheduler
Create a Binding object for a target Pod naming the desired Node directly, skipping the scheduler's own logic entirely
The kubelet does not re-check taints at admission, so the Pod runs even on a NoSchedule-tainted node with no matching toleration
Admission policy engines typically register their webhook on pods CREATE/UPDATE only.
Binding is a different subresource they never inspect, so node-placement policies never see it
Targeting a control plane node places a workload past the taint meant to keep it off.
Combined with a privileged/hostPath Pod spec, this reaches node compromise
Create a workload with serviceAccountName set to a privileged ServiceAccount in the same namespace.
Any controller-backed kind works, since the controller creates the pod.
The token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, or projected explicitly by the manifest.
Reading it back needs a channel.
get on pods/log returns it if the container prints it to stdout.
Without pods/log, write it to /dev/termination-log and read terminated.message from the pod's status, which needs get on pods.
Neither verb is needed if the container sends the token outbound itself, which is why removing pods/log does not close this.
Authenticate as the ServiceAccount with the stolen token.
Rights needed, any one set:
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
· any one workload verb; controller-backed kinds create the pod for you
Create a pod with a gitRepo volume whose repository contains an ext:: command
kubelet runs git clone as root, executing the injected command on the node
Exfiltrate node credentials.
This is T0 on control plane nodes
Note:
The gitRepo driver is disabled by default in 1.33 and GA-locked off in 1.36, so the kubelet will not mount it.
Independently, git 2.12.1+ refuses the ext:: transport by default, blocking the classic CVE-2024-10220 PoC on any normal node regardless of version.
Create a DaemonSet with tolerations using operator Exists so it schedules onto every node, control plane included.
create alone is enough.
The DaemonSet controller creates the pods, so the caller never needs a pods verb.
Put privileged and hostPID in the pod template to reach the host namespaces.
Pod Security Admission evaluates each generated pod against its namespace labels, so a baseline or restricted namespace rejects them and the DaemonSet reports failures instead.
Where the namespace does not enforce pod security, this is root on every node at once.
Patch or update an existing Deployment, DaemonSet, or StatefulSet pod template, or a CronJob's jobTemplate.
Add an attacker sidecar or replace the image.
A Deployment rolls out at once, as do DaemonSets and StatefulSets under the default RollingUpdate strategy.
Under updateStrategy OnDelete they wait for pod recreation.
A CronJob applies the change on its next scheduled run with no extra trigger.
Code runs with the workload's existing ServiceAccount, which is T0 for kube-system workloads.
Rights needed, any one set:
· any one; each owns a pod template
· any one; each owns a pod template
· any one; each owns a pod template
· any one; each owns a pod template
Patch or update an existing ReplicaSet or ReplicationController pod template
The change does not affect already-running pods.
ReplicaSet/RC only apply the template to pods they create afterward
Wait for or force pod replacement, using manual scale, pod deletion, node eviction, or voluntary disruption
The replacement pod is created from the poisoned template and inherits the workload's existing SA
Update alone forces replacement by scaling replicas (0 then 1).
Delete on pods is not required, it only makes replacement immediate without changing the replica count
Rights needed, any one set:
· any one; scaling alone forces a fresh pod onto the template
· any one; scaling alone forces a fresh pod onto the template
· any one; scaling alone forces a fresh pod onto the template
With patch or update on a namespaced object, set metadata.ownerReferences to point at an owner that does not exist, using a resolvable apiVersion and kind with any name and UID.
The garbage collector resolves the reference, confirms the owner is absent, and deletes the object within seconds on the next sync.
No delete permission on the object is required, and no ownership check runs unless the OwnerReferencesPermissionEnforcement admission plugin is enabled, which it is not by default.
Applies to any namespaced resource the principal can patch or update, turning patch rights into a deletion primitive for objects that delete access would otherwise gate.
Rights needed, any one set:
· any one; the primitive works on whichever resource you can patch
· any one; the primitive works on whichever resource you can patch
Note:
Distinct from CVE-2025-5187 (node self-patch to a cluster-scoped owner, patched).
This is intended behavior for namespaced objects, not a CVE, gated only by the OwnerReferencesPermissionEnforcement admission plugin, which is not enabled by default.
Hold update on the pods/status subresource, a grant separate from update on pods itself.
The kubelet normally owns these fields
Forge status.podIP on a Service backend pod.
kube-proxy programs the forged IP into the pod's EndpointSlice and routes live Service traffic there, blackholing or redirecting connections
Or forge the Ready condition, marking a healthy backend NotReady to drop it from the Service, or marking an unhealthy pod Ready to insert it into the ready endpoint set
The kubelet reverts status within seconds, but kube-proxy consumes the forged state during that window.
A slow readiness probe cadence widens it
No write access to Services, Endpoints, or EndpointSlices is required
Note:
Reproduced on both versions.
The EndpointSlice adopted a forged status.podIP verbatim and Service traffic dropped during the window, which the kubelet closed in about two seconds.
Forging the Ready condition did not take on probe-less pods.
Create a PriorityClass with a very high value and preemptionPolicy PreemptLowerPriority.
Create a pod referencing that PriorityClass, with requests large enough that it only fits by evicting a running pod.
The scheduler preempts a lower-priority victim to make room.
This crosses namespaces, because priority is compared node-wide and the scheduler has no notion of which namespace owns the capacity.
The gain is scheduled capacity taken from another team, not credentials or API permissions.
Rights needed:
· both; a PriorityClass preempts nothing until a workload references it, and any controller-backed workload verb does instead of pods
A PodDisruptionBudget only constrains the eviction subresource.
The core pod delete path never consults it
Delete a PDB-protected pod directly with delete on pods.
The API server removes it without checking the budget
This defeats a control that blocks kubectl drain, cluster autoscaler consolidation, and node upgrades from disrupting the pod
Same end state as an allowed eviction but reachable when eviction is refused.
It also forces recreation, pairing with template-poisoning that only lands on pod replacement
Leader election for control-plane components like the scheduler and controller-manager is coordinated through a Lease in kube-system
Patch the component's Lease to set holderIdentity to a foreign value with a fresh renewTime, so the running leader observes a conflicting holder
The real component stops leading and its static pod crashes.
A full crash, restart, and re-election cycle runs for tens of seconds with no scheduler or controller acting
Repeating the patch after each recovery compounds kubelet CrashLoopBackOff, sustaining and lengthening the outage
Enumerate pods to find one running as root, privileged, or with a powerful SA
Exec or attach into it
Steal the mounted SA token, access the node, or move laterally
If the pod sets shareProcessNamespace, the exec'd container sees every sibling container's processes and /proc, letting the attacker read another container's env vars and in-memory secrets
Rights needed, any one set:
· any one; both give container stdio
· any one; both give container stdio
Update pods/ephemeralcontainers to inject a debug container into an existing running Pod.
The debug container runs in the target Pod context and can access the Pod's mounted volumes, ServiceAccount token, and selected process namespace depending on target/runtime support.
Powerful lateral movement inside privileged Pods.
Not the same as kubectl debug node/<name>, which requires create pods and spawns a separate privileged Pod.
Note:
CVE-2023-2727 and CVE-2023-2728 were admission/policy bypasses involving ephemeral containers.
The base RBAC risk is the ability to inject code into an existing Pod.
list or watch secrets cluster-wide, or in kube-system alone, and receive every Secret's data in the response.
Auto-created ServiceAccount token Secrets are gone from modern clusters, so what is harvested is whatever was deliberately stored.
That is the credential material the cluster actually runs on, including registry pull secrets, database and cloud credentials, TLS private keys, and any long-lived token created on purpose.
Revoking the permission afterwards does not undo the exposure.
Every credential returned stays valid until it is individually identified and rotated.
Rights needed, any one set:
· any one; both return Secret data
· any one; both return Secret data
get or list secrets in an accessible namespace
Extract cloud API keys, DB credentials, TLS keys, or kubeconfigs
Use the credentials to escalate inside or outside the cluster
Rights needed, any one set:
· any one
· any one
· any one
get a known service-account-token Secret name in kube-system.
Before 1.24, these were auto-created for every ServiceAccount under a predictable name, so they could simply be guessed
Auto-creation is off by default from 1.24, so on a current cluster the Secret has to already exist, and operator-managed clusters commonly keep one
Where secrets-create is also held, the precondition can be manufactured rather than found.
Annotate a new Secret for a privileged ServiceAccount, and the token controller fills in the token
Reuse the controller ServiceAccount token for cluster-admin-equivalent access.
Tokens from Secrets carry no expiry claim, so the access does not lapse
Rights needed, any one set:
· scoped to kube-system
· needs the Secret name, and a privileged token Secret must already exist
· scoped to kube-system
· enumerates instead, so no name is needed
· scoped to kube-system
· also catches a token Secret created later
· scoped to kube-system
· create manufactures the precondition rather than finding it
Note:
1.24 stopped auto-creating token Secrets, so guessing a predictable name is dead, but the underlying mechanism is not.
Manually created service-account-token Secrets are still populated with non-expiring tokens, and secrets-create in kube-system manufactures the precondition.
Identify a high-privilege ServiceAccount in scope.
The clusterrole-aggregation-controller SA in kube-system has effective cluster-admin rights and is a prime target.
Mint a bound token through the TokenRequest API with an extended lifetime, for example --duration=87600h for ten years.
The API server honors the request unless --service-account-max-token-expiration is set.
kubeadm does not set it, so the full 87600h is issued.
Managed providers commonly do set it.
Authenticate as the ServiceAccount with the minted token.
T0 when targeting kube-system controller service accounts such as clusterrole-aggregation-controller or kube-controller-manager.
Note:
The serviceaccounts/token subresource (TokenRequest API) was promoted to stable in 1.22, making this path available without alpha flags.
No patch exists.
Keep token TTLs short and audit all uses of this verb.
An IAM role must already exist whose trust policy names the cluster OIDC provider plus this namespace and ServiceAccount
Add the eks.amazonaws.com/role-arn annotation naming that role to the ServiceAccount
Pods using the ServiceAccount receive credentials for the role, bounded by whatever that role grants
Rights needed, any one set:
· any one; the annotation is the same write
· any one; the annotation is the same write
An IAM policy binding must already grant this namespace and ServiceAccount roles/iam.workloadIdentityUser on the target GCP service account
Add the iam.gke.io/gcp-service-account annotation naming that service account
Pods using the ServiceAccount obtain GCP tokens carrying exactly that account's IAM roles
Rights needed, any one set:
· any one; the annotation is the same write
· any one; the annotation is the same write
A federated identity credential must already exist on the managed identity, matching the cluster issuer and this namespace and ServiceAccount as subject
Label the ServiceAccount azure.workload.identity/use and annotate it with the identity's client ID
The workload identity webhook projects a token for that managed identity into pods using the ServiceAccount
Rights needed, any one set:
· any one; the label and annotation are the same write
· any one; the label and annotation are the same write
Connect to the exec endpoint via WebSocket.
The HTTP GET upgrade handshake maps to the 'get' verb at the kubelet, so get nodes/proxy alone is sufficient
The kubelet never performs a secondary 'create' check for the exec operation that follows the handshake
Path A, via API server proxy, uses curl with Upgrade: websocket headers.
Auth is logged as a nodes/proxy check only, the actual exec is invisible to audit
Path B is direct to the kubelet on port 10250, using websocat wss://$NODE_IP:10250/exec/....
Zero API server audit events are generated
Execute arbitrary commands in any pod on the node, and steal service account tokens to pivot
Note:
There is no CVE.
The security team calls this working as intended (Jan 2026).
CVE-2020-8559 and CVE-2022-3294 are separate patched issues.
WebSocket exec bypass has no fix.
KEP-2862 adds narrower subresources but does not cover exec/attach/portforward.
Register a fake Node object with status.addresses[InternalIP] = API server internal IP and daemonEndpoints.kubeletEndpoint.Port = 6443
Proxy a request through the fake node by running kubectl get --raw /api/v1/nodes/evil-node/proxy/api/v1/secrets
The API server proxies outbound to that address using its apiserver-kubelet-client certificate, so it authenticates to itself
Before kubeadm 1.29 the certificate carried O=system:masters.
From 1.29 it carried O=kubeadm:cluster-admins, a group bound to cluster-admin, so the inner request still returned full cluster access
Current kubeadm binds the certificate to system:kubelet-api-admin instead, narrowing the inner request to the node API (nodes/proxy, metrics, stats, log) rather than cluster-admin
The redirection itself is unchanged by that fix, so the API server can still be pointed at an attacker-chosen address
Post-fix, the gain is network position rather than RBAC, since triggering it already requires get on nodes/proxy.
The API server reaches addresses the caller cannot, under its own identity
Rights needed:
· both; the Node object is inert until proxied to
Note:
Those releases narrow the certificate to system:kubelet-api-admin, which removes the cluster-admin outcome but not the redirection.
kubernetes#119270 is still open, reopened for audit re-evaluation.
1.32 reached end of life first, so it stays vulnerable.
No CVE covers this path.
Register a fake Node with a DNS rebinding hostname that initially resolves to a valid public IP
API server checks isProxyableHost before connecting.
At check time, DNS resolves to the allowed IP
By connect time, DNS flips to 127.0.0.1 or 169.254.x.x.
The blocklist check is not re-run (TOCTOU)
SSRF reaches loopback services such as kube-proxy :10249 configz, etcd :2379, and control-plane localhost ports
Rights needed:
· both; registering the address and proxying to it
Note:
This is a wontFix.
Fixing requires IP pinning between check and connect, which breaks split-horizon DNS.
Mitigation is dnsmasq min-cache-ttl.
Read kubelet-managed logs for pods and system components on a node.
Through the API server proxy this is gated by nodes/proxy.
nodes/log applies to a direct kubelet connection on port 10250
Applications commonly log credentials, tokens, connection strings, and startup configuration.
Harvest high-value secrets without exec access.
On vulnerable Windows nodes with NodeLogQuery, CVE-2024-9042 made this a command-injection path.
Note:
nodes/log is a real kubelet authorization subresource, but the kubelet checks it against the identity connecting to it, so via the API server proxy nodes/proxy is the effective gate.
CVE-2024-9042 was Windows-specific and is patched.
After compromising one node, patch other nodes with NoSchedule/NoExecute taints
Sensitive pods are forced to reschedule onto the compromised node
Stealing their tokens or intercepting traffic is a separate step needing its own access on the compromised node (secrets read, exec, or hostNetwork).
Tainting only performs the steering
Note:
CVE-2025-5187 (fixed 1.33.4) was a NodeRestriction bypass and applies only to a compromised kubelet identity, not to an RBAC patch grant.
Confirmed still working on 1.36.1 with patch on nodes alone, so treat this path as unpatched.
Create a MutatingWebhookConfiguration with clientConfig pointing at a server you control that the API server can reach.
Set rules to match CREATE on the resources you want, for example pods and secrets, across all namespaces.
Every matched object is sent to the webhook and comes back patched, so sidecars can be injected and securityContext stripped.
failurePolicy sets the trade.
Fail makes the mutation reliable but breaks admission whenever the endpoint is down, while Ignore is quieter and silently skips objects.
sideEffects and admissionReviewVersions must be declared or the API server rejects the configuration.
Create a ValidatingWebhookConfiguration whose rules match the kinds you want to read.
Validating webhooks receive the full object, so Secret and ConfigMap contents leave the cluster on every admission.
The configuration write and each triggering request are both recorded in the audit log.
What no Kubernetes log holds is the payload sent outbound, so the copy itself leaves no cluster-side record.
failurePolicy Ignore keeps admission working when the endpoint is unreachable, which is what makes it quiet.
Delete the ValidatingWebhookConfiguration or MutatingWebhookConfiguration objects that back a policy engine.
Third-party enforcement such as Gatekeeper, Kyverno, or image signature verification stops rejecting anything.
Pod Security Admission is unaffected.
It is a built-in API server controller rather than a webhook, so namespace enforce labels still apply.
On its own this only removes a guard.
It becomes privilege only alongside a verb that creates the workload the webhook was rejecting.
Rights needed, any one set:
· any one; whichever kind backs the policy engine
· any one; whichever kind backs the policy engine
Enumerate User subjects already bound to cluster-admin or other high-privilege ClusterRoles via kubectl get clusterrolebinding.
Create a CSR using a CN matching one of those usernames and request the kubernetes.io/kube-apiserver-client signer.
Approve the CSR.
This requires both update on certificatesigningrequests/approval and approve on signers for that exact signerName.
The built-in signing controller issues a real, CA-signed certificate automatically.
No CA key or extra access is needed from the caller.
The cert authenticates as the matched username, inheriting all its RBAC permissions.
It works until expiry.
Individual revocation requires CA rotation.
Rights needed:
- T2create certificatesigningrequests+T2update certificatesigningrequests/approval+T2approve signers+T3get certificatesigningrequests
· scoped to signerName kubernetes.io/kube-apiserver-client
· all four; no CA key needed
Note:
The CSR API remains dangerous for kube-apiserver client certificates, but the direct built-in system:masters request is rejected by signer policy.
Custom signers or certificates for other bound high-privilege identities can still be escalation paths.
Create an APIService for an API group pointing at an attacker-controlled backend
Requests for that group are proxied with trusted X-Remote-User and X-Remote-Group identity headers.
Caller bearer tokens and TLS certificates are NOT forwarded to the backend
Observe or spoof responses for the claimed API group.
This gives data interception within that group, not a generic credential-theft primitive
Note:
CVE-2018-1002105 and CVE-2022-3172 are patched in all current versions.
The aggregated-APIService MITM path (X-Remote-User/Group headers forwarded to backend with caller identity) is a design property, not a CVE, and remains in all versions where aggregation is enabled.
Create a CRD that registers a conversion webhook pointing at an attacker-controlled server.
Every read and write of that custom resource type is routed through the webhook, which can read and rewrite objects in flight.
The API server requires a CA bundle and TLS, so this needs a serving certificate the API server will accept.
Create a hostPath PersistentVolume mounting node / (or use an existing one via a PVC)
Claim the volume from an otherwise restricted-compliant pod.
PSA does not inspect the PV behind a PVC, so even the restricted profile does not block it
Read or write arbitrary node filesystem paths.
This is T0 on control plane nodes
Or set the PV's claimRef to a victim's PVC name and namespace, including one not yet created, so the victim's future workload silently binds to attacker-controlled storage
Rights needed, any one set:
· create the volume, claim it, mount it
· only where a hostPath PersistentVolume already exists
Note:
The base hostPath PV path is not version-patched, and Pod Security Admission does not block it at any profile.
PSA checks the pod's own volume types, never the PV behind a PVC.
runAsNonRoot only limits which host files are readable, not the mount.
CVE-2021-25741 is a separate, patched issue.
With patch on namespaces, remove the pod-security.kubernetes.io/enforce label from an existing namespace, or set it to privileged, disabling PSA there
Or with create on namespaces, create a fresh namespace with no PSA label at all, which starts permissive
Either path assumes no restrictive cluster-wide default (AdmissionConfiguration).
With such a default, both fall back to it instead of to permissive
Create privileged pods freely in the now-unenforced namespace
Rights needed, any one set:
· any one; relabel a namespace or create an unlabelled one
· any one; relabel a namespace or create an unlabelled one
Create a permissive PSP and bind it, or delete the restrictive PSP
All pod-security guardrails are removed cluster-wide
Create privileged pods.
This only applies to clusters before 1.25
Rights needed, any one set:
· any one
· any one
Note:
PodSecurityPolicy removed in 1.25, replaced by Pod Security Admission (PSA).
PSA enforces the Pod Security Standards (PSS), namely Privileged, Baseline, and Restricted, at namespace level via labels (e.g. pod-security.kubernetes.io/enforce: restricted).
This is EKS-specific and requires the cluster's authentication mode to be CONFIG_MAP or API_AND_CONFIG_MAP.
A cluster fully migrated to API mode ignores this ConfigMap entirely.
If aws-auth doesn't exist yet (self-managed clusters, or a fresh EKS cluster before it's applied), create it directly instead of patching.
Patch or create the aws-auth ConfigMap in kube-system, mapping your AWS IAM ARN to the system:masters group
Authenticate via AWS CLI as cluster-admin
Rights needed, any one set:
· any one write verb on the ConfigMap
· any one write verb on the ConfigMap
· any one write verb on the ConfigMap
Note:
AWS now calls aws-auth deprecated in favor of EKS access entries.
Auth mode moves CONFIG_MAP -> API_AND_CONFIG_MAP -> API only, and never moves back.
Once on API mode, aws-auth is permanently inert.
Delete the NetworkPolicy that isolates the target workload.
A pod is only isolated while some policy selects it.
Once the last selecting policy is gone, the pod accepts traffic from anywhere again.
Where another policy still selects the pod, deletion narrows nothing and the remaining policy keeps enforcing.
Previously unreachable workloads become reachable for lateral movement, and monitoring that relied on the policy blocking traffic sees nothing change.
Create or modify Endpoints or EndpointSlices so a Service resolves to addresses it was never meant to reach, such as node-local services or an attacker-controlled host.
kube-proxy programs those addresses and every client of that Service follows them.
Callers keep using the Service name they trust, which is what makes the Service the confused deputy.
End-to-end delivery depends on the CNI and kube-proxy mode in use, so the reachable destination set varies by cluster.
Rights needed, any one set:
· any one; the mirroring controller links the two
· any one; the mirroring controller links the two
Note:
CVE-2021-25740 is best treated as a confused-deputy risk from write access to Endpoints/EndpointSlices, not a clean version-gated issue.
Kubernetes removed broad Endpoint/EndpointSlice write from default admin/edit roles in v1.22+, but explicit grants remain dangerous.
Use services/proxy through the API server
Reach internal Services that do not require authentication internally
Access dashboards, metrics-server, or internal admin APIs
Read Node objects with get and list on nodes.
The flannel.alpha.coreos.com/public-ip annotation gives each node's underlay IP, backend-data gives its VTEP MAC, and spec.podCIDR gives the pod range
This requires a host with flat L2 reach to the node underlay and CAP_NET_ADMIN, such as a bastion, a compromised VM, or a hostNetwork pod with NET_ADMIN
Build a matching device there with ip link add flannel.1 type vxlan id 1 dev eth0 dstport 8472 nolearning
Add the peer by hand with bridge fdb append <vtep-mac> dev flannel.1 dst <node-underlay-ip> self permanent, then ip neigh add <pod-cidr-base> lladdr <vtep-mac> dev flannel.1
Route into the pod range sourced from the attacker's own underlay IP so replies return, using ip route add <pod-cidr> via <pod-cidr-base> dev flannel.1 onlink src <attacker-underlay-ip>
Reach pod IPs directly, defeating Service-level abstraction.
A Service with no endpoints silently drops traffic while the pod IP behind it still answers
Workloads deliberately hidden from this principal become reachable.
The RBAC half is only get and list on nodes, and the rest of the cost is root on an underlay-connected host.
Rights needed, any one set:
· any one; list needs no node name
· any one; list needs no node name
Note:
Demonstrated end to end in the cited CTF from a bastion with flat L2 to the node underlay.
Not reproducible on kind, where every node is a container on one bridge and there is no separate underlay host, so the lack of a local repro is an environment limit, not evidence against it.
Create or patch a Service and set spec.externalIPs to an address used by another pod, a node, or an off-cluster host.
kube-proxy programs DNAT for that address on every node, sending traffic for it to the Service's endpoints.
NetworkPolicy does not contain this, since policy selects on pod addresses rather than on a Service's externalIPs.
The API server accepts an address already claimed as another Service's ClusterIP.
Which rule wins is decided by the dataplane, not by validation.
Behaviour differs by kube-proxy mode.
iptables favours the first matching rule, while IPVS and eBPF dataplanes resolve collisions on their own terms, so confirm the mode before relying on precedence.
Rights needed, any one set:
· any one; set the field or add it later
· any one; set the field or add it later
Note:
spec.externalIPs is deprecated as of 1.36 and warns on write, but it stays fully functional.
kube-proxy still programs the DNAT identically.
Deprecated is not mitigated (CVE-2020-8554).
Restricting it requires an admission webhook that validates externalIPs.
Update the CoreDNS ConfigMap in kube-system to add a rewrite rule or a stub zone.
CoreDNS reloads its configuration within seconds without a restart.
Every pod in the cluster then resolves the targeted name to an address of your choosing.
Whether that yields credentials depends on the client.
It needs a listener on the redirected address and a client that does not verify the server certificate against the name it asked for.
Rights needed, any one set:
· any one write verb on the Corefile ConfigMap
· any one write verb on the Corefile ConfigMap
· any one write verb on the Corefile ConfigMap
Create a pod with spec.hostNetwork set to true.
The pod shares the node's network namespace and uses the node's address, so it is no longer on the pod network.
NetworkPolicy selects pods by their pod-network identity, so an ordinary namespace policy does not constrain it.
Some CNIs provide a separate host-level firewall that can.
Node-local services become reachable, including the kubelet on 10250 and anything bound to the node's loopback or link-local addresses.
The dependable control is Pod Security Admission at baseline or restricted, which rejects spec.hostNetwork at admission.
Note:
Namespace NetworkPolicy does not select host-network pods.
Whether host traffic can be policed at all is a property of the CNI, not of Kubernetes.
The effective control is Pod Security Admission at baseline or restricted, which rejects spec.hostNetwork.
Create a pod targeting a Windows node with spec.securityContext.windowsOptions.hostProcess: true
Container starts as NT AUTHORITY\SYSTEM in the host network and volume namespaces
Windows HostProcess is blocked by both Baseline and Restricted Pod Security Standards.
Only namespaces enforcing Privileged level or unlabeled namespaces are vulnerable
Full Windows node compromise follows, with lateral move to other workloads or extraction of node credentials
Note:
Windows HostProcess containers were introduced as stable in 1.23.
This is by-design for Windows workloads, so no patch applies.
Linux-only clusters are unaffected.
The risk is specific to mixed OS clusters.
Create a MutatingAdmissionPolicy whose CEL sets privileged and hostPID on every pod spec.
Create a MutatingAdmissionPolicyBinding for it.
A policy with no binding never runs.
Mutating admission completes before validating admission, so Pod Security Admission evaluates the mutated pod and rejects it in a namespace enforcing baseline or restricted.
Where no namespace enforces pod security, the mutation stands and every new pod there runs privileged with host access.
Rights needed:
· both; a policy without a binding is inert
A MutatingAdmissionPolicy must already exist in the cluster with no binding, so it is dormant and not enforced.
Create a MutatingAdmissionPolicyBinding naming it, which matches cluster-wide unless matchResources narrows it.
The policy takes effect on the next CREATE.
What it does is whatever that policy already specifies, which may be harmless.
Note:
Binding write is not a safe reduction from authoring.
Create on mutatingadmissionpolicybindings alone activates a dormant policy with no rights over the policy itself.
Severity is whatever the existing policy already does.
Create a ValidatingAdmissionPolicy with a CEL expression that denies creation of pods or another critical resource.
Create a ValidatingAdmissionPolicyBinding targeting all namespaces.
A policy without a binding is not enforced.
Every matching create is rejected cluster-wide, so deployments, scaling, and recovery all stall until the policy is found and removed.
This denies and never grants.
It is a persistent control-plane denial of service, not a route to any additional identity or credential.
Rights needed:
· both; an unbound policy is not enforced
Patch an existing MutatingWebhookConfiguration to replace the clientConfig.url with an attacker-controlled endpoint
All requests matching the webhook's existing rules are now proxied through the attacker server
Capture SA tokens and secrets.
Silently mutate pod specs or block deployments
Create a ClusterTrustBundle containing a CA certificate you control.
Nothing trusts it on creation.
Only pods that project it through a clusterTrustBundle volume read the bundle at all.
Those pods then accept certificates signed by that CA, so the TLS connections they make can be intercepted.
Workloads using the container image's own CA store, which is the common case, are unaffected.
Note:
Requires the ClusterTrustBundle feature to be enabled and, separately, workloads that project the bundle.
Check with kubectl api-resources | grep trustbundle.
Inert on a cluster where nothing projects it.
Create a kubernetes.io/service-account-token Secret annotated with the target ServiceAccount name.
The token controller populates .data.token with a signed JWT for that ServiceAccount which does not expire.
Population happens after the create response returns, so the token is never in the create result.
Retrieval therefore needs get, list, or watch on secrets, or a pod that mounts the Secret.
create alone mints a credential the caller cannot read.
Authenticate as the target ServiceAccount with the retrieved token.
Rights needed, any one set:
· create mints the token, get reads it back by name
· list returns the data without needing the name
· watch catches the token as the controller fills it in
· a pod that mounts the Secret reads it instead
Note:
The token controller still populates kubernetes.io/service-account-token Secrets created by hand, and long-lived token Secrets remain supported.
Restrict create on secrets and audit for hand-made token Secrets in kube-system.
Proxy an HTTP request through the API server to a known Pod name and port
Reach an internal admin, debug, metrics, or application endpoint not exposed externally.
NetworkPolicies do not apply to API-server-proxied traffic
Extract credentials or invoke privileged application actions via the unauthenticated internal endpoint
Note:
This reaches T0 when the Pod runs as a high-privilege SA and exposes a debug endpoint that returns its mounted token or allows token-issuing actions.
Create a RuntimeClass whose scheduling.tolerations and scheduling.nodeSelector target a control-plane node.
Create a pod that sets only runtimeClassName.
The RuntimeClass admission plugin merges the toleration and nodeSelector into the pod spec during that same request.
The pod's own manifest carries neither field, so manifest review, GitOps diffs, and static scanning miss it.
Validating admission does see the merged spec and can still catch it.
Placement alone is a scheduling-policy bypass.
A standard pod sitting on a control-plane node reads nothing it could not read elsewhere.
It becomes host compromise only with a pod spec that reaches the host, such as privileged or a hostPath mount of /etc/kubernetes.
Rights needed:
· both; the RuntimeClass schedules nothing on its own
Create a StorageClass, or patch an existing one, and set the storageclass.kubernetes.io/is-default-class annotation to make it the cluster default
The API server allows more than one default class, and the DefaultStorageClass plugin breaks the tie by newest creationTimestamp, so a freshly created default wins without touching the existing one
With reclaimPolicy Retain on that class, the provisioned volume and its data survive deletion of the requesting namespace
Every PersistentVolumeClaim that omits storageClassName then binds through the attacker class.
Patch alone also suffices, since it moves the annotation between existing classes with no create
Rights needed, any one set:
· any one
· any one
Create a PodDisruptionBudget whose selector matches the attacker pod, with minAvailable 1 or maxUnavailable 0.
The eviction API refuses to remove the pod and returns 429, so kubectl drain retries and then gives up.
A bare pod is enough.
The budget reports disruptionsAllowed 0 with reason InsufficientPods, which blocks eviction just as effectively.
This binds the Eviction API only.
Deleting the pod outright, drain --disable-eviction, deleting the budget, or deleting the owning controller all get past it.
The value is delay and friction during maintenance or incident response, not durable persistence.
Note:
Distinct from bypassing a PodDisruptionBudget by deleting a protected pod outright.
Here the budget is the attacker's own tool.
Cordoning a node does not evict anything, so only eviction-based workflows are affected.
Create a HorizontalPodAutoscaler targeting a workload with a high minReplicas floor
The controller scales the target to minReplicas immediately, with no metrics-server or metric data required
Used for resource exhaustion, or to multiply the replicas of a workload whose pod template already carries elevated privileges
With create on the pods/eviction subresource alone, and no pod get, list, or delete, evict a named victim pod
Forces the victim workload to reschedule, which can move it onto an attacker-controlled node, or cause targeted disruption
A privileged operator or controller renders an object like a pod from a ConfigMap it owns, then applies it under its own cluster-wide ServiceAccount
Write access to that ConfigMap (get plus patch or update) lets an attacker set the rendered pod privileged with hostPID, or supply arbitrary manifests, without holding those rights directly
For example, the local-path volume provisioner builds its helper pod from its local-path-config ConfigMap.
Poisoning that ConfigMap runs an attacker command as root on a node whenever a PVC is provisioned.
Trigger it with an ordinary PVC and pod in the attacker's own namespace.
The attacker borrows the controller identity.
A GitOps controller is the same pattern, applying attacker manifests cluster-wide
Rights needed, any one set:
· any one write verb on the controller's ConfigMap
· any one write verb on the controller's ConfigMap
· any one write verb on the controller's ConfigMap
· any one write verb on the controller's ConfigMap
Note:
Reaches cluster-admin only where the helper pod can schedule onto a control-plane node and read its admin.conf or CA key.
Where only a worker node is reachable, as on managed control planes, it stops at node compromise.
Guard controller-owned config like kube-system.
Patch the subjects array of an existing RoleBinding to add the attacker.
The roleRef field is immutable, so the Role or ClusterRole it points at cannot be swapped for a more privileged one
Kubernetes applies the same escalation check as create, requiring bind on the referenced Role or ClusterRole, or already holding all of its permissions
Yields whatever that binding already grants, scoped to its namespace.
This is not an independent bypass of the escalation check
The external-attacher CSI sidecar reconciles VolumeAttachment objects and reads status.attached to decide whether a volume is still attached to a node
Write status.attached false through the volumeattachments/status subresource.
The same write aimed at the main resource returns success and is silently dropped
The attacher acts on the reported state and can detach a volume that a running pod still has mounted, so in-flight writes fail with I/O errors
spec is immutable after creation, so an attachment cannot be retargeted to another node or volume.
Only the reported state can be corrupted
Inert without a registered CSI driver running an external-attacher sidecar.
On a cluster with no such driver, the object is reconciled by nothing
Note:
The subresource write is the only path that lands.
A status patch through the main resource is dropped without error.
Whether a detach follows depends on the CSI driver and its attacher, which is why the impact is storage disruption rather than a privilege boundary.
Delete the CRD that a security controller or operator reconciles.
Deleting a CRD garbage-collects every custom resource of that type, so the controller's policy objects go with it.
Whether the controller then fails open or fails closed is implementation-specific.
Losing its input can equally stop it admitting anything.
Treat this as loss of a control and of its configuration, not as a reliable route to a privileged workload.
Create a portforward request against a known pod and port.
The API server opens a stream to the kubelet, which connects to that port inside the pod.
The caller's own machine now talks to the pod's listener directly, with no route into the pod network and no Service or Ingress in front of it.
Databases, admin consoles, and debug handlers bound inside the pod are reachable, including ones bound to loopback that no Service could expose.
The tunnel is not pod-to-pod traffic, so a NetworkPolicy written to isolate the workload does not apply to it.
Unlike the API-server proxy subresources, this needs no HTTP semantics.
Any TCP protocol works, which is what makes it useful against non-HTTP listeners.
Create a second NetworkPolicy selecting the same pods, with an unconditional allow rule for ingress or egress.
Allowed traffic is the union of every policy that selects the pod, so the permissive rule reopens what the restrictive policy closed.
The restrictive policy is untouched and still present, so an audit or GitOps diff of existing objects shows it intact.
Patching an existing policy reaches the same result by adding allow rules or by narrowing its podSelector so it stops selecting the target.
Quieter than deletion, because nothing that was there is missing.
The new object is the only evidence.
Rights needed, any one set:
· any one; add a permissive policy or widen an existing one
· any one; add a permissive policy or widen an existing one
Create a PriorityClass with globalDefault true.
Every pod created afterwards with no priorityClassName inherits that priority, cluster-wide.
No pod create verb is needed.
The effect lands on other people's future workloads, not on one the attacker submits.
Existing pods keep the priority they were admitted with, so the change is invisible until the next rollout.
Only one PriorityClass may be the global default, so setting it also displaces whatever the platform team configured.
