Kubetier

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.

  1. Send Impersonate-User: cluster-admin header to the API server

  2. API server evaluates all RBAC as the impersonated user

  3. Read all secrets or perform any cluster-admin action

K8s docs ↗
  1. Send Impersonate-User together with Impersonate-Group: system:masters.

    Kubernetes rejects group impersonation without an impersonated user.

  2. 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.

  3. Perform any cluster-wide action.

Rights needed:

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.

K8s docs ↗
  1. Impersonate system:serviceaccount:kube-system:clusterrole-aggregation-controller

  2. That SA has escalate on all ClusterRoles by default

  3. Patch own bound ClusterRole to inject */* rules via impersonation

K8s docs ↗
  1. Use the escalate verb to bypass privilege-escalation prevention

  2. Patch own bound ClusterRole to add */* rules (must be a role already bound to you)

  3. 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:

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.

K8s docs ↗
  1. Use escalate on roles to bypass the check that normally prevents writing permissions you do not already hold.

  2. Create or update a Role granting arbitrary permissions in the namespace.

  3. 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:

K8s docs ↗
  1. The bind verb bypasses escalation prevention on ClusterRoleBindings

  2. Create a ClusterRoleBinding from cluster-admin to your own identity

  3. You now hold cluster-admin

Rights needed:

Note:

By design, holding bind on clusterrolebindings intentionally allows self-binding to cluster-admin.

No patch exists.

K8s docs ↗
  1. 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.

  2. bind on clusterroles bypasses the escalation check.

    Create a RoleBinding to cluster-admin or any ClusterRole you don't fully hold.

  3. 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:

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.

K8s docs ↗
  1. Patch the subjects array of an existing ClusterRoleBinding

  2. Kubernetes applies the same escalation check as CREATE, requiring bind on the referenced ClusterRole or already holding all its permissions

  3. Useful for maintenance if you already hold bind.

    It is not an independent bypass of the escalation check

K8s docs ↗
  1. Create or update a ClusterRole with an aggregation label such as rbac.authorization.k8s.io/aggregate-to-admin: true.

  2. The ClusterRole aggregation controller merges its rules into the built-in admin/edit/view role.

  3. 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.

  4. 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:

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.

K8s docs ↗
  1. Identify a running pod you have patch access to, ideally one already carrying a privileged or high-value ServiceAccount.

  2. Patch spec.containers[*].image.

    Container images are among the few pod spec fields Kubernetes allows mutating after creation.

  3. 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.

  4. The kubelet detects the definition change, kills the container, and restarts it running the new attacker-controlled image.

  5. 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.

K8s docs ↗
  1. NetworkPolicy podSelector matches on a running Pod's labels, re-evaluated continuously, not fixed at creation

  2. Patch the label a restrictive NetworkPolicy selects on to a value outside its selector

  3. The same running Pod, same IP, same containers, is no longer covered by that policy's ingress/egress rules

  4. Traffic that was blocked moments earlier now flows, with no image or spec change at all

K8s docs ↗
  1. Patch a NoExecute toleration onto an already-running Pod.

    Tolerations are one of the few pod spec fields addable after creation

  2. An incident responder later taints the suspect node NoExecute to evict everything running on it

  3. The taint manager evicts every Pod without a matching toleration.

    The pre-patched Pod is skipped and keeps running

  4. A backdoor Pod outlives the exact response action meant to remove it from the node

K8s docs ↗
  1. Create a pod with privileged + hostPID or hostPath mounting /

  2. Set spec.nodeName to a specific control plane node to force placement there.

    Taints are scheduler-only checks, so no toleration is needed

  3. chroot or nsenter into the host to gain root on the node

  4. Read /etc/kubernetes/pki, steal kubelet cert or SA tokens.

    This is T0 on the control plane

Rights needed, any one set:

K8s docs ↗
  1. Create a pod using hostNetwork, hostPID, hostIPC or hostPath as available

  2. hostNetwork reaches cloud metadata and the API server from the node network namespace.

    hostPID reads host process env vars

  3. 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).

K8s docs ↗
  1. 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

  2. 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

  3. Create a Binding object for a target Pod naming the desired Node directly, skipping the scheduler's own logic entirely

  4. The kubelet does not re-check taints at admission, so the Pod runs even on a NoSchedule-tainted node with no matching toleration

  5. 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

  6. 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

K8s docs ↗
  1. 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.

  2. The token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, or projected explicitly by the manifest.

  3. Reading it back needs a channel.

    get on pods/log returns it if the container prints it to stdout.

  4. Without pods/log, write it to /dev/termination-log and read terminated.message from the pod's status, which needs get on pods.

  5. Neither verb is needed if the container sends the token outbound itself, which is why removing pods/log does not close this.

  6. Authenticate as the ServiceAccount with the stolen token.

Rights needed, any one set:

K8s docs ↗
  1. Create a pod with a gitRepo volume whose repository contains an ext:: command

  2. kubelet runs git clone as root, executing the injected command on the node

  3. 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.

K8s docs ↗
  1. Create a DaemonSet with tolerations using operator Exists so it schedules onto every node, control plane included.

  2. create alone is enough.

    The DaemonSet controller creates the pods, so the caller never needs a pods verb.

  3. Put privileged and hostPID in the pod template to reach the host namespaces.

  4. 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.

  5. Where the namespace does not enforce pod security, this is root on every node at once.

K8s docs ↗
  1. Patch or update an existing Deployment, DaemonSet, or StatefulSet pod template, or a CronJob's jobTemplate.

  2. Add an attacker sidecar or replace the image.

  3. A Deployment rolls out at once, as do DaemonSets and StatefulSets under the default RollingUpdate strategy.

    Under updateStrategy OnDelete they wait for pod recreation.

  4. A CronJob applies the change on its next scheduled run with no extra trigger.

  5. Code runs with the workload's existing ServiceAccount, which is T0 for kube-system workloads.

Rights needed, any one set:

K8s docs ↗
  1. Patch or update an existing ReplicaSet or ReplicationController pod template

  2. The change does not affect already-running pods.

    ReplicaSet/RC only apply the template to pods they create afterward

  3. Wait for or force pod replacement, using manual scale, pod deletion, node eviction, or voluntary disruption

  4. The replacement pod is created from the poisoned template and inherits the workload's existing SA

  5. 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:

K8s docs ↗
  1. 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.

  2. The garbage collector resolves the reference, confirms the owner is absent, and deletes the object within seconds on the next sync.

  3. 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.

  4. 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:

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.

K8s docs ↗
  1. Hold update on the pods/status subresource, a grant separate from update on pods itself.

    The kubelet normally owns these fields

  2. 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

  3. 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

  4. The kubelet reverts status within seconds, but kube-proxy consumes the forged state during that window.

    A slow readiness probe cadence widens it

  5. 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.

K8s docs ↗
  1. Create a PriorityClass with a very high value and preemptionPolicy PreemptLowerPriority.

  2. Create a pod referencing that PriorityClass, with requests large enough that it only fits by evicting a running pod.

  3. The scheduler preempts a lower-priority victim to make room.

  4. This crosses namespaces, because priority is compared node-wide and the scheduler has no notion of which namespace owns the capacity.

  5. The gain is scheduled capacity taken from another team, not credentials or API permissions.

Rights needed:

K8s docs ↗
  1. A PodDisruptionBudget only constrains the eviction subresource.

    The core pod delete path never consults it

  2. Delete a PDB-protected pod directly with delete on pods.

    The API server removes it without checking the budget

  3. This defeats a control that blocks kubectl drain, cluster autoscaler consolidation, and node upgrades from disrupting the pod

  4. 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

K8s docs ↗
  1. Leader election for control-plane components like the scheduler and controller-manager is coordinated through a Lease in kube-system

  2. Patch the component's Lease to set holderIdentity to a foreign value with a fresh renewTime, so the running leader observes a conflicting holder

  3. 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

  4. Repeating the patch after each recovery compounds kubelet CrashLoopBackOff, sustaining and lengthening the outage

K8s docs ↗
  1. Enumerate pods to find one running as root, privileged, or with a powerful SA

  2. Exec or attach into it

  3. Steal the mounted SA token, access the node, or move laterally

  4. 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:

K8s docs ↗
  1. Update pods/ephemeralcontainers to inject a debug container into an existing running Pod.

  2. 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.

  3. 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.

K8s docs ↗
  1. list or watch secrets cluster-wide, or in kube-system alone, and receive every Secret's data in the response.

  2. Auto-created ServiceAccount token Secrets are gone from modern clusters, so what is harvested is whatever was deliberately stored.

  3. 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.

  4. 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:

K8s docs ↗
  1. get or list secrets in an accessible namespace

  2. Extract cloud API keys, DB credentials, TLS keys, or kubeconfigs

  3. Use the credentials to escalate inside or outside the cluster

Rights needed, any one set:

K8s docs ↗
  1. 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

  2. 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

  3. 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

  4. 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.

K8s docs ↗
  1. 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.

  2. Mint a bound token through the TokenRequest API with an extended lifetime, for example --duration=87600h for ten years.

  3. 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.

  4. Authenticate as the ServiceAccount with the minted token.

  5. 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.

K8s docs ↗
  1. An IAM role must already exist whose trust policy names the cluster OIDC provider plus this namespace and ServiceAccount

  2. Add the eks.amazonaws.com/role-arn annotation naming that role to the ServiceAccount

  3. Pods using the ServiceAccount receive credentials for the role, bounded by whatever that role grants

Rights needed, any one set:

K8s docs ↗
  1. An IAM policy binding must already grant this namespace and ServiceAccount roles/iam.workloadIdentityUser on the target GCP service account

  2. Add the iam.gke.io/gcp-service-account annotation naming that service account

  3. Pods using the ServiceAccount obtain GCP tokens carrying exactly that account's IAM roles

Rights needed, any one set:

K8s docs ↗
  1. A federated identity credential must already exist on the managed identity, matching the cluster issuer and this namespace and ServiceAccount as subject

  2. Label the ServiceAccount azure.workload.identity/use and annotate it with the identity's client ID

  3. The workload identity webhook projects a token for that managed identity into pods using the ServiceAccount

Rights needed, any one set:

K8s docs ↗
  1. 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

  2. The kubelet never performs a secondary 'create' check for the exec operation that follows the handshake

  3. 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

  4. Path B is direct to the kubelet on port 10250, using websocat wss://$NODE_IP:10250/exec/....

    Zero API server audit events are generated

  5. 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.

K8s docs ↗
  1. Register a fake Node object with status.addresses[InternalIP] = API server internal IP and daemonEndpoints.kubeletEndpoint.Port = 6443

  2. Proxy a request through the fake node by running kubectl get --raw /api/v1/nodes/evil-node/proxy/api/v1/secrets

  3. The API server proxies outbound to that address using its apiserver-kubelet-client certificate, so it authenticates to itself

  4. 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

  5. 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

  6. The redirection itself is unchanged by that fix, so the API server can still be pointed at an attacker-chosen address

  7. 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:

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.

K8s docs ↗
  1. Register a fake Node with a DNS rebinding hostname that initially resolves to a valid public IP

  2. API server checks isProxyableHost before connecting.

    At check time, DNS resolves to the allowed IP

  3. By connect time, DNS flips to 127.0.0.1 or 169.254.x.x.

    The blocklist check is not re-run (TOCTOU)

  4. SSRF reaches loopback services such as kube-proxy :10249 configz, etcd :2379, and control-plane localhost ports

Rights needed:

Note:

This is a wontFix.

Fixing requires IP pinning between check and connect, which breaks split-horizon DNS.

Mitigation is dnsmasq min-cache-ttl.

K8s docs ↗
  1. 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

  2. Applications commonly log credentials, tokens, connection strings, and startup configuration.

  3. 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.

K8s docs ↗
  1. After compromising one node, patch other nodes with NoSchedule/NoExecute taints

  2. Sensitive pods are forced to reschedule onto the compromised node

  3. 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.

K8s docs ↗
  1. Create a MutatingWebhookConfiguration with clientConfig pointing at a server you control that the API server can reach.

  2. Set rules to match CREATE on the resources you want, for example pods and secrets, across all namespaces.

  3. Every matched object is sent to the webhook and comes back patched, so sidecars can be injected and securityContext stripped.

  4. 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.

  5. sideEffects and admissionReviewVersions must be declared or the API server rejects the configuration.

K8s docs ↗
  1. Create a ValidatingWebhookConfiguration whose rules match the kinds you want to read.

  2. Validating webhooks receive the full object, so Secret and ConfigMap contents leave the cluster on every admission.

  3. 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.

  4. failurePolicy Ignore keeps admission working when the endpoint is unreachable, which is what makes it quiet.

K8s docs ↗
  1. Delete the ValidatingWebhookConfiguration or MutatingWebhookConfiguration objects that back a policy engine.

  2. Third-party enforcement such as Gatekeeper, Kyverno, or image signature verification stops rejecting anything.

  3. Pod Security Admission is unaffected.

    It is a built-in API server controller rather than a webhook, so namespace enforce labels still apply.

  4. 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:

K8s docs ↗
  1. Enumerate User subjects already bound to cluster-admin or other high-privilege ClusterRoles via kubectl get clusterrolebinding.

  2. Create a CSR using a CN matching one of those usernames and request the kubernetes.io/kube-apiserver-client signer.

  3. Approve the CSR.

    This requires both update on certificatesigningrequests/approval and approve on signers for that exact signerName.

  4. The built-in signing controller issues a real, CA-signed certificate automatically.

    No CA key or extra access is needed from the caller.

  5. The cert authenticates as the matched username, inheriting all its RBAC permissions.

    It works until expiry.

    Individual revocation requires CA rotation.

Rights 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.

K8s docs ↗
  1. Create an APIService for an API group pointing at an attacker-controlled backend

  2. 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

  3. 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.

K8s docs ↗
  1. Create a CRD that registers a conversion webhook pointing at an attacker-controlled server.

  2. Every read and write of that custom resource type is routed through the webhook, which can read and rewrite objects in flight.

  3. The API server requires a CA bundle and TLS, so this needs a serving certificate the API server will accept.

K8s docs ↗
  1. Create a hostPath PersistentVolume mounting node / (or use an existing one via a PVC)

  2. 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

  3. Read or write arbitrary node filesystem paths.

    This is T0 on control plane nodes

  4. 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:

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.

K8s docs ↗
  1. With patch on namespaces, remove the pod-security.kubernetes.io/enforce label from an existing namespace, or set it to privileged, disabling PSA there

  2. Or with create on namespaces, create a fresh namespace with no PSA label at all, which starts permissive

  3. Either path assumes no restrictive cluster-wide default (AdmissionConfiguration).

    With such a default, both fall back to it instead of to permissive

  4. Create privileged pods freely in the now-unenforced namespace

Rights needed, any one set:

K8s docs ↗
  1. Create a permissive PSP and bind it, or delete the restrictive PSP

  2. All pod-security guardrails are removed cluster-wide

  3. Create privileged pods.

    This only applies to clusters before 1.25

Rights needed, any one set:

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).

K8s docs ↗
  1. 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.

  2. 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.

  3. Patch or create the aws-auth ConfigMap in kube-system, mapping your AWS IAM ARN to the system:masters group

  4. Authenticate via AWS CLI as cluster-admin

Rights needed, any one set:

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.

K8s docs ↗
  1. Delete the NetworkPolicy that isolates the target workload.

  2. A pod is only isolated while some policy selects it.

    Once the last selecting policy is gone, the pod accepts traffic from anywhere again.

  3. Where another policy still selects the pod, deletion narrows nothing and the remaining policy keeps enforcing.

  4. Previously unreachable workloads become reachable for lateral movement, and monitoring that relied on the policy blocking traffic sees nothing change.

K8s docs ↗
  1. 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.

  2. kube-proxy programs those addresses and every client of that Service follows them.

  3. Callers keep using the Service name they trust, which is what makes the Service the confused deputy.

  4. 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:

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.

K8s docs ↗
  1. Use services/proxy through the API server

  2. Reach internal Services that do not require authentication internally

  3. Access dashboards, metrics-server, or internal admin APIs

K8s docs ↗
  1. 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

  2. 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

  3. Build a matching device there with ip link add flannel.1 type vxlan id 1 dev eth0 dstport 8472 nolearning

  4. 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

  5. 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>

  6. Reach pod IPs directly, defeating Service-level abstraction.

    A Service with no endpoints silently drops traffic while the pod IP behind it still answers

  7. 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:

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.

K8s docs ↗
  1. Create or patch a Service and set spec.externalIPs to an address used by another pod, a node, or an off-cluster host.

  2. kube-proxy programs DNAT for that address on every node, sending traffic for it to the Service's endpoints.

  3. NetworkPolicy does not contain this, since policy selects on pod addresses rather than on a Service's externalIPs.

  4. The API server accepts an address already claimed as another Service's ClusterIP.

    Which rule wins is decided by the dataplane, not by validation.

  5. 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:

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.

K8s docs ↗
  1. Update the CoreDNS ConfigMap in kube-system to add a rewrite rule or a stub zone.

  2. CoreDNS reloads its configuration within seconds without a restart.

  3. Every pod in the cluster then resolves the targeted name to an address of your choosing.

  4. 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:

K8s docs ↗
  1. Create a pod with spec.hostNetwork set to true.

  2. The pod shares the node's network namespace and uses the node's address, so it is no longer on the pod network.

  3. 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.

  4. Node-local services become reachable, including the kubelet on 10250 and anything bound to the node's loopback or link-local addresses.

  5. 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.

K8s docs ↗
  1. Create a pod targeting a Windows node with spec.securityContext.windowsOptions.hostProcess: true

  2. Container starts as NT AUTHORITY\SYSTEM in the host network and volume namespaces

  3. Windows HostProcess is blocked by both Baseline and Restricted Pod Security Standards.

    Only namespaces enforcing Privileged level or unlabeled namespaces are vulnerable

  4. 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.

K8s docs ↗
  1. Create a MutatingAdmissionPolicy whose CEL sets privileged and hostPID on every pod spec.

  2. Create a MutatingAdmissionPolicyBinding for it.

    A policy with no binding never runs.

  3. Mutating admission completes before validating admission, so Pod Security Admission evaluates the mutated pod and rejects it in a namespace enforcing baseline or restricted.

  4. Where no namespace enforces pod security, the mutation stands and every new pod there runs privileged with host access.

Rights needed:

K8s docs ↗
  1. A MutatingAdmissionPolicy must already exist in the cluster with no binding, so it is dormant and not enforced.

  2. Create a MutatingAdmissionPolicyBinding naming it, which matches cluster-wide unless matchResources narrows it.

  3. 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.

K8s docs ↗
  1. Create a ValidatingAdmissionPolicy with a CEL expression that denies creation of pods or another critical resource.

  2. Create a ValidatingAdmissionPolicyBinding targeting all namespaces.

    A policy without a binding is not enforced.

  3. Every matching create is rejected cluster-wide, so deployments, scaling, and recovery all stall until the policy is found and removed.

  4. 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:

K8s docs ↗
  1. Patch an existing MutatingWebhookConfiguration to replace the clientConfig.url with an attacker-controlled endpoint

  2. All requests matching the webhook's existing rules are now proxied through the attacker server

  3. Capture SA tokens and secrets.

    Silently mutate pod specs or block deployments

K8s docs ↗
  1. Create a ClusterTrustBundle containing a CA certificate you control.

  2. Nothing trusts it on creation.

    Only pods that project it through a clusterTrustBundle volume read the bundle at all.

  3. Those pods then accept certificates signed by that CA, so the TLS connections they make can be intercepted.

  4. 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.

K8s docs ↗
  1. Create a kubernetes.io/service-account-token Secret annotated with the target ServiceAccount name.

  2. The token controller populates .data.token with a signed JWT for that ServiceAccount which does not expire.

  3. Population happens after the create response returns, so the token is never in the create result.

  4. 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.

  5. Authenticate as the target ServiceAccount with the retrieved token.

Rights needed, any one set:

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.

K8s docs ↗
  1. Proxy an HTTP request through the API server to a known Pod name and port

  2. Reach an internal admin, debug, metrics, or application endpoint not exposed externally.

    NetworkPolicies do not apply to API-server-proxied traffic

  3. 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.

K8s docs ↗
  1. Create a RuntimeClass whose scheduling.tolerations and scheduling.nodeSelector target a control-plane node.

  2. Create a pod that sets only runtimeClassName.

    The RuntimeClass admission plugin merges the toleration and nodeSelector into the pod spec during that same request.

  3. 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.

  4. Placement alone is a scheduling-policy bypass.

    A standard pod sitting on a control-plane node reads nothing it could not read elsewhere.

  5. 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:

K8s docs ↗
  1. Create a StorageClass, or patch an existing one, and set the storageclass.kubernetes.io/is-default-class annotation to make it the cluster default

  2. 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

  3. With reclaimPolicy Retain on that class, the provisioned volume and its data survive deletion of the requesting namespace

  4. 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:

K8s docs ↗
  1. Create a PodDisruptionBudget whose selector matches the attacker pod, with minAvailable 1 or maxUnavailable 0.

  2. The eviction API refuses to remove the pod and returns 429, so kubectl drain retries and then gives up.

  3. A bare pod is enough.

    The budget reports disruptionsAllowed 0 with reason InsufficientPods, which blocks eviction just as effectively.

  4. 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.

  5. 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.

K8s docs ↗
  1. Create a HorizontalPodAutoscaler targeting a workload with a high minReplicas floor

  2. The controller scales the target to minReplicas immediately, with no metrics-server or metric data required

  3. Used for resource exhaustion, or to multiply the replicas of a workload whose pod template already carries elevated privileges

K8s docs ↗
  1. With create on the pods/eviction subresource alone, and no pod get, list, or delete, evict a named victim pod

  2. Forces the victim workload to reschedule, which can move it onto an attacker-controlled node, or cause targeted disruption

K8s docs ↗
  1. 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

  2. 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

  3. For example, the local-path volume provisioner builds its helper pod from its local-path-config ConfigMap.

  4. Poisoning that ConfigMap runs an attacker command as root on a node whenever a PVC is provisioned.

  5. 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:

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.

K8s docs ↗
  1. 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

  2. Kubernetes applies the same escalation check as create, requiring bind on the referenced Role or ClusterRole, or already holding all of its permissions

  3. Yields whatever that binding already grants, scoped to its namespace.

    This is not an independent bypass of the escalation check

K8s docs ↗
  1. The external-attacher CSI sidecar reconciles VolumeAttachment objects and reads status.attached to decide whether a volume is still attached to a node

  2. Write status.attached false through the volumeattachments/status subresource.

    The same write aimed at the main resource returns success and is silently dropped

  3. 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

  4. spec is immutable after creation, so an attachment cannot be retargeted to another node or volume.

    Only the reported state can be corrupted

  5. 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.

K8s docs ↗
  1. Delete the CRD that a security controller or operator reconciles.

  2. Deleting a CRD garbage-collects every custom resource of that type, so the controller's policy objects go with it.

  3. Whether the controller then fails open or fails closed is implementation-specific.

    Losing its input can equally stop it admitting anything.

  4. Treat this as loss of a control and of its configuration, not as a reliable route to a privileged workload.

K8s docs ↗
  1. 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.

  2. 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.

  3. Databases, admin consoles, and debug handlers bound inside the pod are reachable, including ones bound to loopback that no Service could expose.

  4. The tunnel is not pod-to-pod traffic, so a NetworkPolicy written to isolate the workload does not apply to it.

  5. 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.

K8s docs ↗
  1. Create a second NetworkPolicy selecting the same pods, with an unconditional allow rule for ingress or egress.

  2. Allowed traffic is the union of every policy that selects the pod, so the permissive rule reopens what the restrictive policy closed.

  3. The restrictive policy is untouched and still present, so an audit or GitOps diff of existing objects shows it intact.

  4. Patching an existing policy reaches the same result by adding allow rules or by narrowing its podSelector so it stops selecting the target.

  5. Quieter than deletion, because nothing that was there is missing.

    The new object is the only evidence.

Rights needed, any one set:

K8s docs ↗
  1. Create a PriorityClass with globalDefault true.

  2. Every pod created afterwards with no priorityClassName inherits that priority, cluster-wide.

  3. No pod create verb is needed.

    The effect lands on other people's future workloads, not on one the attacker submits.

  4. Existing pods keep the priority they were admitted with, so the change is invisible until the next rollout.

  5. Only one PriorityClass may be the global default, so setting it also displaces whatever the platform team configured.

K8s docs ↗